# Mediawiki Extension Development

> Guide developers through creating MediaWiki extensions from setup to publication. Covers extension architecture, implementing hooks/special pages/APIs, database integration, localization, testing, and Wikimedia deployment. Use when developers want to create new extensions, add functionality to MediaWiki, or prepare extensions for production deployment.

- Skill: `santhoshtr/mediawiki-extension-development` (Agent Skill, multi-file: 17 files)
- Install (CLI): `npx skillmds@latest add santhoshtr/mediawiki-extension-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/santhoshtr/mediawiki-extension-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: santhoshtr (https://skillmd.com/u/santhoshtr)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/santhoshtr/mediawiki-extension-development

---


# MediaWiki Extension Development

## Overview

MediaWiki extensions add custom functionality, modify behavior, and integrate new features into MediaWiki wikis. This skill provides comprehensive guidance for developing extensions following MediaWiki best practices, from initial setup through production deployment.

Extensions can add special pages, modify wiki behavior through hooks, provide APIs, extend wiki markup, manage content, and integrate with external services. The development process involves setup, implementation, localization, testing, and publication.

## When to Use This Skill

Use this skill when developers request help with:

- Creating a new MediaWiki extension from scratch
- Setting up extension structure and registration
- Implementing hooks to modify MediaWiki behavior
- Adding special pages for custom functionality
- Creating API modules (Action API or REST API)
- Extending wiki markup with parser functions or tags
- Adding database tables for extension data
- Implementing localization (i18n) for messages
- Preparing extensions for Wikimedia deployment
- Understanding MediaWiki extension architecture
- Debugging extension issues
- Following MediaWiki coding conventions

## Quick Start

### Minimal Extension Setup

Create a functional extension in 5 minutes:

1. **Create directory structure**:
```bash
cd extensions/
mkdir -p MyExtension/includes
cd MyExtension
```

2. **Create `extension.json`**:
```json
{
	"name": "MyExtension",
	"author": "Your Name",
	"url": "https://www.mediawiki.org/wiki/Extension:MyExtension",
	"description": "Brief description of what your extension does",
	"version": "1.0.0",
	"license-name": "GPL-2.0-or-later",
	"type": "other",
	"manifest_version": 2,
	"AutoloadNamespaces": {
		"MediaWiki\\Extension\\MyExtension\\": "includes/"
	}
}
```

3. **Enable in `LocalSettings.php`**:
```php
wfLoadExtension( 'MyExtension' );
```

4. **Verify**: Visit `Special:Version` to see your extension listed.

This creates a valid, registered extension. Now add functionality through extension points.

## Core Workflows

### Workflow 1: Creating a New Extension

Follow this complete workflow for new extensions:

#### Step 1: Set Up Development Environment

Ensure a working MediaWiki installation:

```bash
# Clone MediaWiki if needed
git clone https://gerrit.wikimedia.org/r/mediawiki/core.git
cd core
composer install
php maintenance/install.php --dbtype=sqlite --dbpath=$(pwd)/data \
    --pass=AdminPassword "My Wiki" "Admin"
```

**Development settings** - Add to `LocalSettings.php`:
```php
// Disable caching during development
$wgMainCacheType = CACHE_NONE;
$wgCacheDirectory = false;

// Enable error reporting
error_reporting( -1 );
ini_set( 'display_errors', 1 );
$wgShowExceptionDetails = true;
$wgShowDBErrorBacktrace = true;
$wgShowSQLErrors = true;

// Enable debugging
$wgDebugToolbar = true;
$wgShowDebug = true;
```

#### Step 2: Use BoilerPlate as Starting Point

MediaWiki provides the BoilerPlate extension as a template:

```bash
cd extensions/
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/BoilerPlate.git MyExtension
cd MyExtension
rm -rf .git
```

**Customize the extension**:
1. Rename namespace in files from `MediaWiki\Extension\BoilerPlate` to `MediaWiki\Extension\MyExtension`
2. Update `extension.json` with your details
3. Update `i18n/*.json` files with your messages
4. Modify or remove example code

#### Step 3: Define Extension Metadata

Complete the `extension.json` file with all relevant fields:

**Essential fields**:
```json
{
	"name": "MyExtension",
	"author": ["Your Name", "Contributor Name"],
	"url": "https://www.mediawiki.org/wiki/Extension:MyExtension",
	"description": "One-sentence description",
	"version": "1.0.0",
	"license-name": "GPL-2.0-or-later",
	"type": "other",
	"manifest_version": 2
}
```

**Type field** - Choose appropriate type:
- `specialpage` - Adds special pages
- `parserhook` - Extends wiki markup
- `media` - Media handlers
- `semantic` - Semantic extensions
- `skin` - Skins (for skin development)
- `api` - API extensions
- `other` - General extensions

**Additional useful fields**:
```json
{
	"requires": {
		"MediaWiki": ">= 1.39.0"
	},
	"AutoloadNamespaces": {
		"MediaWiki\\Extension\\MyExtension\\": "includes/"
	},
	"config": {
		"MyExtensionSomeSetting": {
			"value": true,
			"description": "Description of this configuration option"
		}
	},
	"MessagesDirs": {
		"MyExtension": ["i18n"]
	}
}
```

See `assets/templates/extension.json` for a complete template.

#### Step 4: Implement Core Functionality

Choose appropriate extension points based on your needs. See **Workflow 2-6** for specific implementations.

#### Step 5: Add Localization

Implement internationalization for all user-facing text:

1. Create `i18n/en.json`:
```json
{
	"myextension-desc": "Extension description for Special:Version",
	"myextension-hello": "Hello, world!",
	"myextension-greeting": "Hello, $1! Welcome to $2."
}
```

2. Create `i18n/qqq.json` for documentation:
```json
{
	"myextension-desc": "{{desc|name=MyExtension|url=https://...}}",
	"myextension-hello": "Simple greeting message shown on example page",
	"myextension-greeting": "Personalized greeting. Parameters:\n* $1 - User name\n* $2 - Wiki name"
}
```

3. Register in `extension.json`:
```json
{
	"MessagesDirs": {
		"MyExtension": ["i18n"]
	}
}
```

**For API-specific messages**, use separate directory:
```json
{
	"MessagesDirs": {
		"MyExtension": ["i18n", "i18n/api"]
	}
}
```

Create `i18n/api/en.json` for API help messages with `apihelp-` prefix:
```json
{
	"apihelp-myaction-summary": "Performs a custom action",
	"apihelp-myaction-param-text": "Text to process",
	"apihelp-myaction-example-1": "Process the text 'hello'"
}
```

**For special page aliases**, use `ExtensionMessagesFiles`:
```json
{
	"ExtensionMessagesFiles": {
		"MyExtensionAlias": "MyExtension.i18n.alias.php"
	}
}
```

Create `MyExtension.i18n.alias.php`:
```php
<?php
$specialPageAliases = [];

$specialPageAliases['en'] = [
	'MyPage' => [ 'MyPage', 'My Page' ],
];

$specialPageAliases['nl'] = [
	'MyPage' => [ 'MijnPagina' ],
];
```

See `assets/templates/MyExtension.i18n.alias.php` for complete template.

4. Use in code:
```php
// In special pages or contexts with IContextSource
$output->addHTML( $this->msg( 'myextension-hello' )->escaped() );

// With parameters
$greeting = $this->msg( 'myextension-greeting', $userName, $wikiName )->text();

// Standalone (not context-aware)
$message = wfMessage( 'myextension-hello' )->inLanguage( 'de' )->text();
```

See `references/localization-guide.md` for complete i18n patterns.

#### Step 6: Test and Debug

**Testing approaches**:
1. **Manual testing**: Enable extension, test features in browser
2. **PHPUnit tests**: Create tests in `tests/phpunit/`
3. **Integration tests**: Test with other extensions enabled
4. **Parser tests**: For parser hooks, create `.txt` files in `tests/parser/`

**Debugging tools**:
```php
// Use MediaWiki's debug log
wfDebugLog( 'MyExtension', 'Debug message here' );

// Check variables
wfDebug( print_r( $variable, true ) );

// Use MediaWiki's logger
LoggerFactory::getInstance( 'MyExtension' )->info( 'Log message' );
```

**Common issues**:
- Extension not appearing: Check `wfLoadExtension()` in LocalSettings.php
- Hooks not firing: Verify hook name and signature
- Namespace errors: Check autoloading configuration
- Messages not showing: Verify MessagesDirs and file location

### Workflow 2: Implementing Hooks

Hooks allow extensions to modify MediaWiki behavior at specific points.

#### Understanding Hooks

MediaWiki core and extensions emit hooks at key execution points. Extensions register handler functions to run when hooks fire.

**Hook types**:
1. **Run hooks**: Execute handler, continue regardless of return value
2. **Abort hooks**: Handler can stop execution by returning false
3. **Processing hooks**: Handler modifies data passed by reference

**Finding hooks**: See `references/hooks-reference.md` for common hooks or search [MediaWiki documentation](https://www.mediawiki.org/wiki/Manual:Hooks).

#### Registering Hooks

**Method 1: Function-based (simple)**:

In `extension.json`:
```json
{
	"Hooks": {
		"BeforePageDisplay": "MyExtensionHooks::onBeforePageDisplay"
	}
}
```

Create handler class:
```php
<?php
namespace MediaWiki\Extension\MyExtension;

class MyExtensionHooks {
	/**
	 * Handler for BeforePageDisplay hook
	 * @param \OutputPage $out
	 * @param \Skin $skin
	 */
	public static function onBeforePageDisplay( $out, $skin ) {
		// Add custom CSS
		$out->addModuleStyles( 'ext.myextension.styles' );
	}
}
```

**Method 2: HookHandler (recommended for MW 1.35+)**:

In `extension.json`:
```json
{
	"HookHandlers": {
		"main": {
			"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\MainHookHandler"
		}
	},
	"Hooks": {
		"BeforePageDisplay": "main",
		"ParserFirstCallInit": "main"
	}
}
```

Create handler:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Hooks;

use MediaWiki\Hook\BeforePageDisplayHook;
use MediaWiki\Hook\ParserFirstCallInitHook;

class MainHookHandler implements BeforePageDisplayHook, ParserFirstCallInitHook {

	public function onBeforePageDisplay( $out, $skin ): void {
		$out->addModuleStyles( 'ext.myextension.styles' );
	}

	public function onParserFirstCallInit( $parser ) {
		$parser->setHook( 'myextension', [ $this, 'renderTag' ] );
	}

	public function renderTag( $input, array $args, $parser, $frame ) {
		return '<div class="myextension">' . htmlspecialchars( $input ) . '</div>';
	}
}
```

**Hook handler advantages**:
- Type safety through interfaces
- Better IDE support and autocomplete
- Dependency injection support
- Cleaner separation of concerns

#### Organizing Multiple Hook Handlers

For larger extensions, organize hooks into multiple handler classes by domain or purpose:

**extension.json**:
```json
{
	"HookHandlers": {
		"ui": {
			"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\UIHooks",
			"services": [ "PermissionManager" ]
		},
		"parser": {
			"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\ParserHooks"
		},
		"database": {
			"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\DatabaseHooks"
		}
	},
	"Hooks": {
		"BeforePageDisplay": "ui",
		"SkinTemplateNavigation::Universal": "ui",
		"ParserFirstCallInit": "parser",
		"ParserGetVariableValueSwitch": "parser",
		"LoadExtensionSchemaUpdates": "database"
	}
}
```

**Handle deprecated hooks**:
```json
{
	"Hooks": {
		"SomeOldHook": {
			"handler": "main",
			"deprecated": true
		},
		"NewReplacementHook": "main"
	}
}
```

Marking hooks as deprecated prevents deprecation warnings when MediaWiki phases out old hooks.

#### Common Hook Patterns

**Modify page content before display**:
```php
// Hook: BeforePageDisplay
public function onBeforePageDisplay( $out, $skin ): void {
	$out->addJsConfigVars( 'myExtensionConfig', [
		'setting' => true
	] );
	$out->addModules( 'ext.myextension.init' );
}
```

**Add custom user rights**:
```php
// Hook: UserGetRights
public function onUserGetRights( $user, &$rights ) {
	if ( $user->isRegistered() ) {
		$rights[] = 'myextension-use-feature';
	}
}
```

**Validate page saves**:
```php
// Hook: EditFilterMergedContent
public function onEditFilterMergedContent( $context, $content, $status, $summary, $user, $minoredit ) {
	if ( strpos( $content->getText(), 'forbidden-word' ) !== false ) {
		$status->fatal( 'myextension-forbidden-word-error' );
		return false; // Abort save
	}
	return true;
}
```

**Track custom actions**:
```php
// Hook: PageSaveComplete
public function onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult ) {
	// Log to custom table or external service
	$dbw = wfGetDB( DB_PRIMARY );
	$dbw->insert(
		'myextension_edits',
		[
			'page_id' => $wikiPage->getId(),
			'user_id' => $user->getId(),
			'timestamp' => $dbw->timestamp()
		],
		__METHOD__
	);
}
```

See `references/hooks-reference.md` for 50+ common hooks with examples and `assets/templates/HookHandlerExample.php` for complete implementation.

### Workflow 3: Creating Special Pages

Special pages provide custom functionality accessible via `Special:PageName`.

#### Basic Special Page

1. **Create special page class**:

Create `includes/Specials/SpecialMyPage.php`:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Specials;

use SpecialPage;

class SpecialMyPage extends SpecialPage {

	public function __construct() {
		parent::__construct( 'MyPage' );
	}

	public function execute( $subPage ) {
		$this->setHeaders();
		$this->outputHeader();

		$out = $this->getOutput();
		$out->setPageTitle( $this->msg( 'myextension-mypage-title' ) );
		$out->addWikiMsg( 'myextension-mypage-intro' );

		$out->addHTML( '<p>Hello from MyPage!</p>' );
	}

	protected function getGroupName() {
		return 'other';
	}
}
```

2. **Register in `extension.json`**:
```json
{
	"SpecialPages": {
		"MyPage": "MediaWiki\\Extension\\MyExtension\\Specials\\SpecialMyPage"
	},
	"MessagesDirs": {
		"MyExtension": ["i18n"]
	}
}
```

3. **Add messages** to `i18n/en.json`:
```json
{
	"mypage": "My Page",
	"myextension-mypage-title": "My Custom Page",
	"myextension-mypage-intro": "This is an example special page."
}
```

4. **Access**: Navigate to `Special:MyPage` on your wiki.

#### Special Page with Form

For pages accepting user input:

```php
<?php
namespace MediaWiki\Extension\MyExtension\Specials;

use HTMLForm;
use SpecialPage;

class SpecialMyForm extends SpecialPage {

	public function __construct() {
		parent::__construct( 'MyForm' );
	}

	public function execute( $subPage ) {
		$this->setHeaders();
		$this->checkPermissions();

		$formDescriptor = [
			'username' => [
				'type' => 'text',
				'label-message' => 'myextension-form-username',
				'required' => true,
			],
			'message' => [
				'type' => 'textarea',
				'label-message' => 'myextension-form-message',
				'rows' => 5,
			],
			'sendcopy' => [
				'type' => 'check',
				'label-message' => 'myextension-form-sendcopy',
			],
		];

		$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
		$htmlForm
			->setSubmitTextMsg( 'myextension-form-submit' )
			->setSubmitCallback( [ $this, 'onSubmit' ] )
			->show();
	}

	public function onSubmit( array $data ) {
		// Process form submission
		$username = $data['username'];
		$message = $data['message'];

		// Do something with the data
		$this->getOutput()->addWikiMsg( 'myextension-form-success', $username );

		return true; // Or return Status object
	}

	protected function getGroupName() {
		return 'users';
	}
}
```

#### Restricted Special Page

Require specific permissions:

```php
public function __construct() {
	parent::__construct(
		'MyAdminPage',
		'myextension-admin' // Required user right
	);
}

public function execute( $subPage ) {
	$this->setHeaders();
	$this->checkPermissions(); // Verifies user has required right

	// Page content for authorized users only
}
```

Define the user right in `extension.json`:
```json
{
	"AvailableRights": [
		"myextension-admin"
	],
	"GroupPermissions": {
		"sysop": {
			"myextension-admin": true
		}
	}
}
```

See `assets/templates/SpecialPageExample.php` for complete implementations.

#### Includable Special Pages

Includable special pages can be transcluded into wiki pages using `{{Special:PageName}}` syntax:

**Create includable special page**:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Specials;

use IncludableSpecialPage;

class SpecialMyIncludable extends IncludableSpecialPage {

	public function __construct() {
		parent::__construct( 'MyIncludable' );
	}

	public function execute( $par = null ) {
		if ( $this->including() ) {
			// Content when transcluded: {{Special:MyIncludable}}
			$this->getOutput()->addWikiTextAsInterface(
				$this->msg( 'myextension-transcluded', $par )->text()
			);
		} else {
			// Content when viewed directly at Special:MyIncludable
			$this->setHeaders();
			$this->getOutput()->addWikiTextAsInterface(
				$this->msg( 'myextension-direct-view' )->text()
			);
		}
	}
}
```

**Register in extension.json**:
```json
{
	"SpecialPages": {
		"MyIncludable": "MediaWiki\\Extension\\MyExtension\\Specials\\SpecialMyIncludable"
	}
}
```

**Usage in wiki pages**:
- Direct link: `[[Special:MyIncludable]]`
- Transclusion: `{{Special:MyIncludable}}`
- With parameter: `{{Special:MyIncludable/Item123}}`

See `assets/templates/SpecialIncludableExample.php` for complete implementation.

### Workflow 4: Creating API Modules

Extensions can provide API endpoints for programmatic access.

#### Action API Module

The Action API follows MediaWiki's `api.php` pattern.

1. **Create API module**:

Create `includes/Api/ApiMyAction.php`:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Api;

use ApiBase;

class ApiMyAction extends ApiBase {

	public function execute() {
		$params = $this->extractRequestParams();

		$result = [
			'success' => true,
			'input' => $params['text'],
			'output' => strtoupper( $params['text'] )
		];

		$this->getResult()->addValue( null, $this->getModuleName(), $result );
	}

	public function getAllowedParams() {
		return [
			'text' => [
				ApiBase::PARAM_TYPE => 'string',
				ApiBase::PARAM_REQUIRED => true,
			],
		];
	}

	public function getExamplesMessages() {
		return [
			'action=myaction&text=hello'
				=> 'apihelp-myaction-example-1',
		];
	}
}
```

2. **Register in `extension.json`**:
```json
{
	"APIModules": {
		"myaction": "MediaWiki\\Extension\\MyExtension\\Api\\ApiMyAction"
	}
}
```

3. **Use the API**:
```
GET /api.php?action=myaction&text=hello&format=json
```

Response:
```json
{
	"myaction": {
		"success": true,
		"input": "hello",
		"output": "HELLO"
	}
}
```

#### API Query List Modules

Query list modules return lists of items and are accessed via `action=query&list=modulename`:

**Create list module**:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Api;

use ApiQueryBase;
use ApiBase;

class ApiQueryMyList extends ApiQueryBase {

	public function __construct( $query, $moduleName ) {
		// Third parameter is prefix for parameters (e.g., 'ml' for 'mllimit')
		parent::__construct( $query, $moduleName, 'ml' );
	}

	public function execute() {
		$params = $this->extractRequestParams();
		$db = $this->getDB();

		$res = $db->select(
			'myextension_items',
			[ 'item_id', 'item_name' ],
			[],
			__METHOD__,
			[ 'LIMIT' => $params['limit'] ]
		);

		$items = [];
		foreach ( $res as $row ) {
			$items[] = [
				'id' => (int)$row->item_id,
				'name' => $row->item_name
			];
		}

		$this->getResult()->addValue( [ 'query', $this->getModuleName() ], 'items', $items );
	}

	public function getAllowedParams() {
		return [
			'limit' => [
				ApiBase::PARAM_TYPE => 'integer',
				ApiBase::PARAM_DFLT => 10,
				ApiBase::PARAM_MIN => 1,
				ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
			],
		];
	}

	protected function getExamplesMessages() {
		return [
			'action=query&list=mylist' => 'apihelp-query+mylist-example-1',
		];
	}
}
```

**Register in extension.json**:
```json
{
	"APIListModules": {
		"mylist": "MediaWiki\\Extension\\MyExtension\\Api\\ApiQueryMyList"
	}
}
```

**API module types**:
- `APIModules` - Base action modules (e.g., `action=myaction`)
- `APIListModules` - Query list modules (e.g., `action=query&list=mylist`)
- `APIMetaModules` - Query meta modules (e.g., `action=query&meta=myinfo`)
- `APIQueryModules` - Query property modules (e.g., `action=query&prop=myprop`)

See `assets/templates/ApiQueryListExample.php` for complete implementation.

#### REST API Module

Modern REST API (MediaWiki 1.35+):

1. **Create handler**:

Create `includes/Rest/HelloHandler.php`:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Rest;

use MediaWiki\Rest\SimpleHandler;
use Wikimedia\ParamValidator\ParamValidator;

class HelloHandler extends SimpleHandler {

	public function run( $name ) {
		return [
			'message' => "Hello, $name!",
			'timestamp' => wfTimestampNow()
		];
	}

	public function getParamSettings() {
		return [
			'name' => [
				self::PARAM_SOURCE => 'path',
				ParamValidator::PARAM_TYPE => 'string',
				ParamValidator::PARAM_REQUIRED => true,
			],
		];
	}
}
```

2. **Register in `extension.json`**:
```json
{
	"RestRoutes": [
		{
			"path": "/myextension/hello/{name}",
			"method": "GET",
			"class": "MediaWiki\\Extension\\MyExtension\\Rest\\HelloHandler"
		}
	]
}
```

3. **Use the REST API**:
```
GET /rest.php/myextension/hello/World
```

Response:
```json
{
	"message": "Hello, World!",
	"timestamp": "20240209123456"
}
```

#### API Security Best Practices

- **Validate all inputs**: Use `ParamValidator` and type checking
- **Require tokens for writes**: Use `needsToken()` for modifications
- **Check permissions**: Use `mustBePosted()` and verify user rights
- **Rate limiting**: Consider using MediaWiki's rate limiting hooks
- **Sanitize output**: Escape user-provided content

#### Custom Page Actions

Actions add custom tabs and functionality to wiki pages (similar to "edit", "history"):

**Create action class**:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Actions;

use FormlessAction;

class MyCustomAction extends FormlessAction {

	public function getName() {
		return 'myaction';
	}

	protected function getDescription() {
		return ''; // Empty string to disable subtitle
	}

	public function onView() {
		return null;
	}

	public function show() {
		parent::show();
		$this->getOutput()->addWikiTextAsInterface(
			$this->msg( 'myextension-action-text', $this->getTitle()->getPrefixedText() )->text()
		);
	}
}
```

**Register in extension.json**:
```json
{
	"Actions": {
		"myaction": "MediaWiki\\Extension\\MyExtension\\Actions\\MyCustomAction"
	}
}
```

**Add action tab to navigation** (in hook handler):
```php
public function onSkinTemplateNavigation__Universal( $skin, &$content_navigation ): void {
	$title = $skin->getTitle();

	if ( $title && !$title->isSpecialPage() ) {
		$content_navigation['actions']['myaction'] = [
			'class' => $skin->getRequest()->getText( 'action' ) === 'myaction' ? 'selected' : false,
			'text' => $skin->msg( 'myextension-action-tab' )->text(),
			'href' => $title->getLocalURL( 'action=myaction' ),
		];
	}
}
```

**Access**: Navigate to any page and add `?action=myaction` to the URL, or click the tab.

See `assets/templates/ActionExample.php` for complete implementation.

### Workflow 5: Extending Wiki Markup

Add custom tags, parser functions, and variables to wiki content.

#### Custom Tags

Register custom XML-style tags like `<myextension>content</myextension>`:

```php
// In hook handler
public function onParserFirstCallInit( $parser ) {
	$parser->setHook( 'myextension', [ $this, 'renderMyTag' ] );
}

public function renderMyTag( $input, array $args, $parser, $frame ) {
	// $input: Content between tags
	// $args: Tag attributes (e.g., <myextension foo="bar">)

	$output = '<div class="myextension-output">';
	$output .= htmlspecialchars( $input );
	$output .= '</div>';

	// Parse wikitext in input
	// $parsed = $parser->recursiveTagParse( $input, $frame );

	return $output;
}
```

Usage in wiki:
```wiki
<myextension>This content will be processed</myextension>
```

#### Parser Functions

Register template-style functions like `{{#myfunction:arg1|arg2}}`:

```php
public function onParserFirstCallInit( $parser ) {
	$parser->setFunctionHook( 'myfunction', [ $this, 'renderFunction' ] );
}

public function renderFunction( $parser, $arg1 = '', $arg2 = '' ) {
	// Process arguments
	$result = strtoupper( $arg1 ) . ' - ' . strtolower( $arg2 );

	// Return array with output and options
	return [ $result, 'noparse' => true, 'isHTML' => false ];
}
```

Usage in wiki:
```wiki
{{#myfunction:Hello|WORLD}}
```

Output: `HELLO - world`

#### Magic Words / Variables

Add variables like `{{CURRENTYEAR}}`:

```php
public function onMagicWordwgVariableIDs( &$variableIDs ) {
	$variableIDs[] = 'mycustomvar';
}

public function onParserGetVariableValueSwitch( $parser, &$cache, $magicWordId, &$ret ) {
	if ( $magicWordId === 'mycustomvar' ) {
		$ret = 'Custom Value';
	}
}
```

Register in `extension.json`:
```json
{
	"Hooks": {
		"MagicWordwgVariableIDs": "main",
		"ParserGetVariableValueSwitch": "main"
	}
}
```

Usage in wiki:
```wiki
{{MYCUSTOMVAR}}
```

#### Localizing Parser Extensions

For parser functions and magic words, use `ExtensionMessagesFiles` to provide translations:

**Create `MyExtension.i18n.magic.php`**:
```php
<?php
$magicWords = [];

/** English */
$magicWords['en'] = [
	'myfunction' => [ 0, 'myfunction' ],
	'mycustomvar' => [ 0, 'MYCUSTOMVAR' ],
];

/** Dutch */
$magicWords['nl'] = [
	'myfunction' => [ 0, 'mijnfunctie' ],
	'mycustomvar' => [ 0, 'MIJNCUSTOMVAR' ],
];
```

**Register in extension.json**:
```json
{
	"ExtensionMessagesFiles": {
		"MyExtensionMagic": "MyExtension.i18n.magic.php"
	}
}
```

This allows users to use localized magic word names in their language.

See `assets/templates/MyExtension.i18n.magic.php` for complete template and MediaWiki documentation for parser extension patterns.

### Workflow 6: Adding Database Tables

Extensions should use custom tables, never modify core tables.

#### Schema Definition

1. **Create SQL schema** in `sql/tables.json` (preferred, MW 1.39+):
```json
[
	{
		"name": "myextension_items",
		"columns": [
			{
				"name": "item_id",
				"type": "integer",
				"options": { "autoincrement": true, "notnull": true, "unsigned": true }
			},
			{
				"name": "item_name",
				"type": "string",
				"options": { "length": 255, "notnull": true }
			},
			{
				"name": "item_created",
				"type": "mwtimestamp",
				"options": { "notnull": true }
			}
		],
		"indexes": [
			{
				"name": "item_name",
				"columns": ["item_name"],
				"unique": false
			}
		],
		"pk": ["item_id"]
	}
]
```

Or create SQL files for different databases:
- `sql/mysql/tables.sql`
- `sql/postgres/tables.sql`
- `sql/sqlite/tables.sql`

Example `sql/mysql/tables.sql`:
```sql
CREATE TABLE /*_*/myextension_items (
	item_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
	item_name VARCHAR(255) NOT NULL,
	item_created BINARY(14) NOT NULL,
	INDEX item_name (item_name)
) /*$wgDBTableOptions*/;
```

2. **Register in hook handler**:
```php
public function onLoadExtensionSchemaUpdates( $updater ) {
	$dir = dirname( __DIR__, 2 );

	// For abstract schema (MW 1.39+)
	$updater->addExtensionTable(
		'myextension_items',
		"$dir/sql/tables.json"
	);

	// Or for SQL files
	// $updater->addExtensionTable(
	// 	'myextension_items',
	// 	"$dir/sql/{$updater->getDB()->getType()}/tables.sql"
	// );
}
```

3. **Run database updates**:
```bash
php maintenance/update.php
```

#### Multi-Database Support

Support MySQL, PostgreSQL, and SQLite using abstract schema (recommended):

**Directory structure**:
```
sql/
├── tables.json              # Abstract schema
├── tables-generated.sql     # Generated MySQL SQL
├── postgres/
│   └── tables-generated.sql
└── sqlite/
    └── tables-generated.sql
```

**Generate SQL from abstract schema**:
```bash
php maintenance/generateSchemaSql.php --json sql/tables.json --sql sql/tables-generated.sql
php maintenance/generateSchemaSql.php --json sql/tables.json --sql sql/postgres/tables-generated.sql --type postgres
php maintenance/generateSchemaSql.php --json sql/tables.json --sql sql/sqlite/tables-generated.sql --type sqlite
```

**Hook handler with database detection**:
```php
public function onLoadExtensionSchemaUpdates( $updater ) {
	$dbType = $updater->getDB()->getType();
	$dir = __DIR__ . '/../sql';

	if ( $dbType !== 'mysql' ) {
		$dir .= "/$dbType";
	}

	$updater->addExtensionTable( 'myextension_items', "$dir/tables-generated.sql" );
}
```

This pattern ensures your extension works across different database systems used by MediaWiki installations.

#### Using the Database

**Insert data**:
```php
$dbw = wfGetDB( DB_PRIMARY );
$dbw->insert(
	'myextension_items',
	[
		'item_name' => 'Example',
		'item_created' => $dbw->timestamp()
	],
	__METHOD__
);
$itemId = $dbw->insertId();
```

**Query data**:
```php
$dbr = wfGetDB( DB_REPLICA );
$result = $dbr->select(
	'myextension_items',
	[ 'item_id', 'item_name', 'item_created' ],
	[ 'item_name' => 'Example' ],
	__METHOD__,
	[ 'ORDER BY' => 'item_created DESC', 'LIMIT' => 10 ]
);

foreach ( $result as $row ) {
	echo $row->item_name;
}
```

**Update data**:
```php
$dbw = wfGetDB( DB_PRIMARY );
$dbw->update(
	'myextension_items',
	[ 'item_name' => 'Updated Name' ],
	[ 'item_id' => 123 ],
	__METHOD__
);
```

**Delete data**:
```php
$dbw = wfGetDB( DB_PRIMARY );
$dbw->delete(
	'myextension_items',
	[ 'item_id' => 123 ],
	__METHOD__
);
```

#### Database Best Practices

- **Always use DB_REPLICA for reads**: Only use DB_PRIMARY for writes
- **Use parameter binding**: Never concatenate user input into queries
- **Prefix table names**: Use `myextension_` prefix for all tables
- **Use /*_*/ in SQL**: Allows wiki-specific table prefixes
- **Add indexes**: Index frequently queried columns
- **Use transactions**: For multi-step operations
- **Check existence**: Use `tableExists()` before querying

See `references/database-schema.md` for detailed patterns and migration examples.

## Extension Points Reference

MediaWiki provides numerous extension points. Brief overview:

### General Extension Points

- **Hooks**: Inject code at 500+ points in MediaWiki execution
- **Domain Events**: React to state changes (MW 1.44+)
- **Jobs**: Queue asynchronous background tasks
- **Content Handlers**: Support custom content models

### Page Extension Points

- **Special Pages**: Custom pages like `Special:MyPage`
- **Actions**: Custom page actions (`?action=myaction`)
- **Tracking Categories**: Automatically categorize pages

### API Extension Points

- **Action API Modules**: Extend `api.php`
- **REST API Handlers**: Modern REST endpoints
- **API Metadata**: Provide API documentation

### Content Extension Points

- **Parser Hooks**: Tags, functions, variables
- **Content Models**: Support non-wikitext content
- **Media Handlers**: Support custom media types

### Moderation Extension Points

- **Log Types**: Custom log entries
- **Recent Changes Flags**: Custom RC indicators
- **Revision Tags**: Annotate revisions

### Authentication Extension Points

- **Auth Providers**: Custom login mechanisms
- **Session Providers**: Custom session handling

See `references/extension-points.md` for complete list with examples.

## Best Practices

### Code Organization

**Directory structure**:
```
MyExtension/
├── extension.json          # Registration and config
├── includes/               # PHP code
│   ├── Api/               # API modules
│   ├── Hooks/             # Hook handlers
│   ├── Specials/          # Special pages
│   └── ...
├── resources/             # Frontend assets
│   ├── js/                # JavaScript
│   ├── css/               # Styles
│   └── ...
├── i18n/                  # Localization
│   ├── en.json
│   └── qqq.json
├── sql/                   # Database schemas
├── tests/                 # Tests
│   ├── phpunit/
│   └── parser/
└── README.md              # Documentation
```

**Naming conventions**:
- **Namespaces**: `MediaWiki\Extension\MyExtension\`
- **Classes**: `UpperCamelCase`
- **Methods**: `camelCase`
- **Constants**: `UPPER_SNAKE_CASE`
- **Config**: `$wgMyExtensionSettingName`
- **Messages**: `myextension-message-key`

### Security

**Always sanitize output**:
```php
// For HTML output
$output->addHTML( Html::element( 'div', [], $userInput ) );

// For plain text
$output->addWikiTextAsInterface( $userInput );

// For messages
$this->msg( 'key', $userInput )->escaped();
```

**Validate input**:
```php
// Check user permissions
if ( !$this->getUser()->isAllowed( 'myextension-action' ) ) {
	throw new PermissionsError( 'myextension-action' );
}

// Validate tokens for state-changing operations
$request = $this->getRequest();
if ( !$this->getUser()->matchEditToken( $request->getVal( 'token' ) ) ) {
	// Invalid token
}
```

**Database security**:
```php
// Use parameterized queries
$dbr->select(
	'myextension_items',
	'*',
	[ 'item_name' => $userInput ], // Automatically escaped
	__METHOD__
);

// NEVER do this
// $sql = "SELECT * FROM myextension_items WHERE item_name = '$userInput'";
```

### Performance

**Caching**:
```php
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$key = $cache->makeKey( 'myextension', 'data', $itemId );

$data = $cache->getWithSetCallback(
	$key,
	$cache::TTL_HOUR,
	function () use ( $itemId ) {
		// Expensive operation
		return $this->fetchData( $itemId );
	}
);
```

**Lazy loading**:
```php
// Don't load heavy resources on every page
public function onBeforePageDisplay( $out, $skin ): void {
	// Only load on specific pages
	if ( $out->getTitle()->inNamespace( NS_MAIN ) ) {
		$out->addModules( 'ext.myextension.init' );
	}
}
```

**Database optimization**:
- Use `DB_REPLICA` for reads
- Add indexes for frequently queried columns
- Use `select()` with specific fields, not `*`
- Consider batching operations

### MediaWiki Coding Conventions

Follow [MediaWiki coding conventions](https://www.mediawiki.org/wiki/Manual:Coding_conventions):

- **Indentation**: Tabs, not spaces
- **Line length**: Soft limit 100 characters
- **Bracing**: Opening brace on same line
- **Spacing**: Space after keywords, around operators
- **Documentation**: PHPDoc for all public methods

**Example**:
```php
<?php
namespace MediaWiki\Extension\MyExtension;

/**
 * Example class demonstrating coding conventions
 */
class ExampleClass {

	/**
	 * Process some data
	 *
	 * @param string $input User input
	 * @param array $options Processing options
	 * @return string Processed output
	 */
	public function process( $input, array $options = [] ) {
		if ( empty( $input ) ) {
			return '';
		}

		$result = strtoupper( $input );

		if ( isset( $options['reverse'] ) && $options['reverse'] ) {
			$result = strrev( $result );
		}

		return $result;
	}
}
```

## Common Patterns

### Pattern 1: User Preference

Add custom user preferences:

```php
// Hook: GetPreferences
public function onGetPreferences( $user, &$preferences ) {
	$preferences['myextension-feature-enabled'] = [
		'type' => 'toggle',
		'label-message' => 'myextension-pref-enable',
		'section' => 'personal/myextension',
	];
}

// Check preference in code
$user = $this->getUser();
$enabled = $user->getOption( 'myextension-feature-enabled', false );
```

### Pattern 2: Page Property

Store custom metadata on pages:

```php
// Set page property
$wikiPage->setProperty( 'myextension_validated', '1' );

// Query pages with property
$dbr = wfGetDB( DB_REPLICA );
$result = $dbr->select(
	[ 'page', 'page_props' ],
	'page_title',
	[
		'pp_propname' => 'myextension_validated',
		'pp_value' => '1'
	],
	__METHOD__,
	[],
	[
		'page_props' => [ 'JOIN', 'page_id=pp_page' ]
	]
);
```

### Pattern 3: ResourceLoader Module

Load JavaScript and CSS:

In `extension.json`:
```json
{
	"ResourceModules": {
		"ext.myextension.init": {
			"scripts": "resources/js/init.js",
			"styles": "resources/css/styles.css",
			"dependencies": ["mediawiki.api"],
			"messages": ["myextension-js-message"]
		},
		"ext.myextension.module": {
			"packageFiles": [
				"resources/js/module.js",
				"resources/js/utils.js"
			],
			"dependencies": ["mediawiki.util"],
			"messages": ["myextension-module-message"]
		}
	}
}
```

**packageFiles vs scripts**:
- `scripts`: Traditional concatenated JavaScript files
- `packageFiles`: Modern ES6 modules with `require()` and `module.exports`

With packageFiles, use modular JavaScript:
```javascript
// resources/js/utils.js
module.exports = {
	formatData: function(data) {
		return data.toUpperCase();
	}
};

// resources/js/module.js
var utils = require('./utils.js');

module.exports = {
	init: function() {
		var formatted = utils.formatData('hello');
		mw.log(formatted); // "HELLO"
	}
};
```

In hook:
```php
public function onBeforePageDisplay( $out, $skin ): void {
	$out->addModules( 'ext.myextension.init' );
}
```

In JavaScript:
```javascript
mw.loader.using('ext.myextension.init', function() {
	// Access messages
	var message = mw.message('myextension-js-message').text();

	// Use MW API
	new mw.Api().get({
		action: 'myaction',
		text: 'hello'
	}).done(function(data) {
		console.log(data);
	});
});
```

### Pattern 4: Configuration Variables

Define user-configurable settings:

In `extension.json`:
```json
{
	"config": {
		"MyExtensionMaxItems": {
			"value": 100,
			"description": "Maximum number of items to display"
		},
		"MyExtensionEnableFeature": {
			"value": true,
			"description": "Enable experimental feature"
		}
	}
}
```

In code:
```php
$config = MediaWikiServices::getInstance()->getMainConfig();
$maxItems = $config->get( 'MyExtensionMaxItems' );
$featureEnabled = $config->get( 'MyExtensionEnableFeature' );
```

User can override in `LocalSettings.php`:
```php
$wgMyExtensionMaxItems = 200;
$wgMyExtensionEnableFeature = false;
```

### Pattern 5: Dependency Injection

Use MediaWiki services (MW 1.35+):

Create service in `includes/Services/MyService.php`:
```php
<?php
namespace MediaWiki\Extension\MyExtension\Services;

class MyService {
	private $cache;

	public function __construct( $cache ) {
		$this->cache = $cache;
	}

	public function doSomething() {
		// Use injected dependencies
	}
}
```

Register in `extension.json`:
```json
{
	"ServiceWiringFiles": [
		"includes/ServiceWiring.php"
	]
}
```

Create `includes/ServiceWiring.php`:
```php
<?php
use MediaWiki\Extension\MyExtension\Services\MyService;
use MediaWiki\MediaWikiServices;

return [
	'MyExtension.MyService' => function ( MediaWikiServices $services ) {
		return new MyService(
			$services->getMainWANObjectCache()
		);
	},
];
```

Use in code:
```php
$service = MediaWikiServices::getInstance()->getService( 'MyExtension.MyService' );
$service->doSomething();
```

## Testing and Debugging

### PHPUnit Tests

Create tests in `tests/phpunit/`:

```php
<?php
namespace MediaWiki\Extension\MyExtension\Tests;

use MediaWikiIntegrationTestCase;

class MyExtensionTest extends MediaWikiIntegrationTestCase {

	public function testBasicFunction() {
		$result = strtoupper( 'hello' );
		$this->assertSame( 'HELLO', $result );
	}

	public function testWithDatabase() {
		$this->tablesUsed[] = 'myextension_items';

		$dbw = $this->db;
		$dbw->insert(
			'myextension_items',
			[ 'item_name' => 'Test' ],
			__METHOD__
		);

		$count = $dbw->selectField(
			'myextension_items',
			'COUNT(*)',
			[],
			__METHOD__
		);

		$this->assertGreaterThan( 0, $count );
	}
}
```

Run tests:
```bash
php tests/phpunit/phpunit.php extensions/MyExtension/tests/phpunit/
```

### Parser Tests

Create `tests/parser/myExtensionTests.txt`:

```
!! test
MyExtension tag basic
!! wikitext
<myextension>test content</myextension>
!! html
<div class="myextension-output">test content</div>
!! end
```

Run parser tests:
```bash
php tests/parser/parserTests.php --file=extensions/MyExtension/tests/parser/myExtensionTests.txt
```

### QUnit JavaScript Tests

Test JavaScript modules using MediaWiki's QUnit integration:

**Register tests in extension.json**:
```json
{
	"QUnitTestModule": {
		"localBasePath": "tests/qunit/",
		"remoteExtPath": "MyExtension/tests/qunit/",
		"scripts": [
			"ext.myextension.test.js"
		],
		"dependencies": [
			"ext.myextension.module"
		]
	}
}
```

**Create test file** `tests/qunit/ext.myextension.test.js`:
```javascript
QUnit.module( 'ext.myextension', QUnit.newMwEnvironment(), function ( hooks ) {

	hooks.beforeEach( function () {
		mw.config.set( {
			wgMyExtensionSetting: 'test-value'
		} );
	} );

	QUnit.test( 'Module exports expected interface', function ( assert ) {
		var myModule = require( 'ext.myextension.module' );

		assert.strictEqual( typeof myModule.init, 'function', 'init method exists' );
	} );

	QUnit.test( 'Function produces correct output', function ( assert ) {
		var myModule = require( 'ext.myextension.module' );
		var result = myModule.formatData( 'hello' );

		assert.strictEqua

…(truncated)
