Building Custom MediaWiki Extensions with Composer

Why Composer Matters for MediaWiki Extensions

When you start fiddling with a MediaWiki installation, the first thing that usually pops up is the endless list of requires in the readme. “Download this library, drop it in extensions/,” you’re told. That works for a quick hack, but it quickly becomes a nightmare once you need to upgrade or share the code with a colleague. Composer, the PHP dependency manager that’s been around for over a decade, solves that mess by turning every library into a version‑controlled package that can be pulled in automatically.

In practice, that means you can focus on the actual feature you’re building – say a custom parser tag or an API module – while Composer deals with the boring bits: pulling in Packagist or private repositories, respecting semantic version constraints, and generating an autoloader that plays nicely with MediaWiki’s own loading logic.

The Project Skeleton

First off, don’t start by writing a giant extension.json file and then wonder where to put your PHP classes. Instead, lay down a small directory tree that mirrors a typical Composer package. Something like this:

myextension/
├─ composer.json
├─ extension.json
├─ src/
│ ├─ Hooks/
│ │ └─ ParserFirstCallInit.php
│ └─ MyExtension.php
└─ i18n/
└─ en.json

Notice the src/ folder – that’s where the autoloader will look for namespaced classes. MediaWiki will still expect extension.json at the root, but the rest of the code can be nicely encapsulated.

Composer Basics for Extensions

Open a fresh composer.json and sprinkle in the essential bits. Below is a minimal example that works for MediaWiki 1.35+ (the LTS release). Pay attention to the type field: setting it to mediawiki-extension lets extensions that rely on mediawiki/extension-distributor recognize your package later on.

{ "name": "yourname/myextension", "description": "A custom MediaWiki extension built with Composer", "type": "mediawiki-extension", "require": { "php": ">=7.4", "mediawiki/core": "^1.35" }, "autoload": { "psr-4": { "YourName\\MyExtension\\": "src/" } }, "extra": { "branch-alias": { "dev-master": "1.0-dev" } } }

That file does a few things: it tells Composer that we need at least PHP 7.4, it locks the MediaWiki core version to the LTS series, and it registers a PSR‑4 autoloader that maps the YourName\\MyExtension\\ namespace to the src/ folder.

Running Composer in the Extension Directory

From your terminal, navigate to the extension root and run:

composer install

The command pulls in any third‑party packages (none in the skeleton, but you might add Guzzle, Symfony components, etc.) and drops an vendor/ folder next to your code. MediaWiki already knows how to load classes from vendor/autoload.php when you add a single line to LocalSettings.php, but there’s a more elegant way: let the extension bootstrap do it.

Hooking into MediaWiki the Composer Way

Imagine you want to add a custom parser hook that converts <mytag> into something fancy. In a classic extension you would drop a function into MyExtension.php and register it in extension.json. With Composer you can still keep that registration, but the handler lives in a namespaced class.

Defining the Hook in extension.json

Here’s the relevant snippet – note the fully qualified class name and static method:

{ "Hooks": { "ParserFirstCallInit": "YourName\\MyExtension\\Hooks\\ParserFirstCallInit::onParserFirstCallInit" } }

The Actual Hook Handler

Now create src/Hooks/ParserFirstCallInit.php:

setHook( 'mytag', [ self::class, 'renderMyTag' ] ); return true; // tell MediaWiki we didn’t abort } public static function renderMyTag( string $content, array $attributes, Parser $parser, ?\PPFrame $frame ): string { // Very naive sanitisation – just an example. $text = htmlspecialchars( $content, ENT_QUOTES, 'UTF-8' ); return "$text"; } }

The code is deliberately straightforward. You could, of course, inject services via the MediaWiki service container, but that would require a bit more setup in extension.json – something we’ll touch on later.

Integrating Composer‑Managed Services

Composer shines when your extension needs third‑party libraries. Let’s say you want to fetch data from a remote API using Guzzle. First, add it to the require section:

composer require guzzlehttp/guzzle:^7.0

Composer updates composer.json automatically, and you’ll see Guzzle under vendor/guzzlehttp/guzzle. To make the client available throughout the extension, you can register it as a service in extension.json. This is a bit more advanced, so bear with me.

Defining a Service

Append this to extension.json under the top‑level key ServiceWiring (MediaWiki 1.35+ supports it):

{ "ServiceWiring": { "MyExtension.GuzzleClient": { "class": "GuzzleHttp\\Client", "factory": "YourName\\MyExtension\\ServiceFactory::createGuzzleClient" } } }

Now write the factory:

getMainConfig()->get( 'MyExtensionGuzzleOptions' ); return new Client( $options ); } }

That tiny bit of boilerplate lets you grab the Guzzle client in any other hook or API module via:

$client = $services->getService( 'MyExtension.GuzzleClient' );

Notice how the service name mirrors the key we used in extension.json. It’s a pattern you’ll see across many extensions that need external libs – keep it consistent and future‑proof.

Packaging and Distribution

One of the reasons developers shy away from Composer is the belief that they have to publish their extension to Packagist. That’s not true. You can host a private repository – even a simple Git server – and reference it from composer.json using the repositories block. Example:

{ "repositories": [ { "type": "vcs", "url": "https://github.com/yourname/myextension" } ], "require": { "yourname/myextension": "dev-master" } }

When another wiki runs composer require yourname/myextension, Composer will clone the repo, run composer install inside it (respecting any dependencies you’ve defined), and place the code under extensions/MyExtension – assuming you’ve set up the extra field in the package’s composer.json to indicate the target path.

Extra Field for Destination

MediaWiki’s own composer-plugin can understand a special extra.mediawiki-extension-dir key. Add it to your extension’s composer.json like so:

{ "extra": { "mediawiki-extension-dir": "MyExtension" } }

Now the plugin will automatically copy the extension into the correct directory when the root composer install runs. It’s a neat trick for large deployments that keep all extensions under version control but install them via Composer on each server.

Testing Your Extension

Testing may feel like an afterthought, yet Composer makes it almost painless. Because your code lives under src/ and follows PSR‑4, you can drop a tests/ folder with PHPUnit tests, add phpunit/phpunit as a dev requirement, and run them from the extension root:

composer require --dev phpunit/phpunit:^9.5
vendor/bin/phpunit tests

A quick test example for the parser hook:

createMock( Parser::class ), null ); $this->assertStringContainsString( 'Hello', $output ); } }

Running the suite gives you instant feedback, and because Composer locks the PHPUnit version, you won’t be surprised by “works on my machine” errors when you move to another server.

Common Pitfalls and How to Avoid Them

  • Forgetting to register the autoloader. If MediaWiki can’t find your class, double‑check that vendor/autoload.php is required. Adding require_once __DIR__ . '/vendor/autoload.php'; at the top of extension.json (via AutoloadClasses) works.
  • Version mismatches. MediaWiki core evolves fast. Pin your mediawiki/core requirement to the LTS branch you’re targeting, otherwise Composer might try to pull a newer version that your server can’t run.
  • Namespace typos. Because Composer’s autoloader is case‑sensitive, a small typo in the namespace declaration will lead to an obscure “class not found” error. A quick composer dump-autoload -o can highlight missing mappings.
  • Direct file includes. Resist the urge to include_once PHP files manually. Let Composer do the heavy lifting – it’s more maintainable.

Wrapping Up

Building a custom MediaWiki extension with Composer isn’t a magic bullet, but it does turn a chaotic pile of .php files into a tidy, version‑controlled package. You get the benefits of modern PHP practices – namespacing, autoloading, and dependency isolation – without sacrificing compatibility with the MediaWiki ecosystem.

From the skeleton layout to service wiring and testing, the steps outlined above give you a repeatable workflow that scales from a single‑person sandbox to a multi‑developer deployment. Once you’ve got the basics down, experimenting with more advanced Composer plugins (like cweagans/composer-patches for quick hot‑fixes) becomes a natural next step.

So, whether you’re adding a tiny parser tweak or a full‑blown API client, let Composer be the foundation you build on. It keeps your code clean, your dependencies tidy, and your upgrade path clear – all things any MediaWiki maintainer can appreciate.

Subscribe to MediaWiki Tips and Tricks

Don’t miss out on the latest articles. Sign up now to get access to the library of members-only articles.
jamie@example.com
Subscribe