Mastering MediaWiki: Advanced Extension Development Techniques
Why “just another hook” isn’t enough
When you first dip your toes into MediaWiki extension land, the official guide feels like a friendly handbook – “add a hook, return true, you’re done”. In reality, the real magic happens when you start juggling several hooks, services, and caching layers at once. It’s a bit like trying to keep a kitchen full of sous‑chefs from stepping on each other’s toes while the dinner rush is on.
Hook choreography: ordering matters
Most newbies think ParserFirstCallInit is the perfect place to register a parser function. Sure, it works, but if you also need to tweak the output after the parser has done its thing, you’ll want ParserAfterParse or even OutputPageParserOutput. The order in which these fire can change the final HTML – sometimes dramatically.
// Register a parser function early
$wgHooks['ParserFirstCallInit'][] = function ( Parser $parser ) {
$parser->setFunctionHook( 'myfunc', 'MyExtension::myFunc' );
return true;
};
// Later, modify the output
$wgHooks['ParserAfterParse'][] = function ( Parser $parser, $text ) {
// Example: wrap all tables in a div for styling
$text = preg_replace( '/', '</div>', $text );
return true;
};
Notice the two separate hook registrations. If you jam them together, you might end up with a preg_replace that never sees the table because the parser function hasn’t run yet. That’s the kind of subtle bug that makes you pull your hair out at 2 a.m.
Service wiring – the new “dependency injection” playground
Since MediaWiki 1.35, extensions are encouraged to use the ExtensionRegistry and the built‑in service container. It feels a bit like moving from a toolbox with a single hammer to a full‑blown workshop where every tool knows its own place.
Defining services in extension.json
Here’s a minimal snippet that registers a service for a custom API module:
{
"name": "MyAdvancedExtension",
"version": "1.0.0",
"author": "Jane Doe",
"services": {
"MyAdvancedExtension.ApiHandler": {
"class": "MyAdvancedExtension\\Api\\Handler",
"arguments": [ "@HttpRequestFactory", "@UserFactory" ]
}
}
}
Notice the @ prefixes – they tell MediaWiki to pull existing services from the container. If you forget the @, the container will try to instantiate a brand‑new object, which can lead to duplicate DB connections. I’ve tripped over that one more times than I’d like to admit.
Fetching a service in code
Instead of reaching for a global variable, you now ask the container:
$handler = MediaWikiServices::getInstance()->getService( 'MyAdvancedExtension.ApiHandler' );
$handler->doSomethingFancy();
It’s a tiny change, but it makes unit testing a breeze. You can swap the real handler for a mock in a test suite without touching the global state. That’s the kind of “future‑proofing” that feels rewarding after a long day of debugging.
Performance tricks that actually matter
Let’s be honest: most extensions are written by hobbyists who never think about a wiki with a million page views per day. When you do, you start looking at caching not as a nice‑to‑have, but as a lifeline.
ObjectCache vs. $wgMemc
Older tutorials still mention $wgMemc. It works, but it’s a blunt instrument. The modern ObjectCache API gives you namespacing, TTL handling, and automatic fallback to the appropriate backend (Redis, Memcached, or even the file cache).
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$key = $cache->makeKey( 'myext', 'userlist', $userId );
$data = $cache->getWithSetCallback(
$key,
$cache::TTL_HOUR,
function () use ( $userId ) {
// Expensive DB query here
return $this->loadUserDataFromDB( $userId );
}
);
Notice the makeKey call – it automatically prefixes the key with the wiki ID, preventing cross‑wiki collisions on a shared cache server. I once saw a production outage because two wikis on the same cluster used the same raw key “stats”. Don’t be that guy.
Lazy loading with DeferredUpdates
If you need to write to the DB after a page save, consider DeferredUpdates::addUpdate. It queues the work until the request is about to finish, letting the user see the saved page faster.
DeferredUpdates::addUpdate( new class implements DeferredUpdate {
public function doUpdate() {
// Heavy logging or analytics here
$db = wfGetDB( DB_MASTER );
$db->insert( 'myext_log', [ 'log_time' => wfTimestampNow() ] );
}
} );
It’s a bit of a mouthful, but the pattern is simple: do the cheap stuff now, defer the expensive stuff. The result? A snappier UI and a happier community.
Permission fine‑tuning – beyond “user” and “sysop”
MediaWiki’s permission system is often reduced to “who can edit?” and “who can delete?”. In a complex extension you’ll want granular rights like “myext‑view‑stats” or “myext‑edit‑config”. The best‑practices page recommends defining them in extension.json:
{
"Permissions": {
"myext-view-stats": {
"desc": "View advanced statistics",
"right": "myext-view-stats"
},
"myext-edit-config": {
"desc": "Edit MyExtension configuration",
"right": "myext-edit-config"
}
}
}
Then, in your hook, check the right:
if ( !$user->isAllowed( 'myext-edit-config' ) ) {
throw new PermissionsError( 'myext-edit-config' );
}
It’s a tiny extra step, but it saves you from a future where a sysop accidentally breaks the site by toggling a hidden setting. Trust me, you’ll thank yourself when the community asks “who gave them that power?” and you can point to a clean, documented right.
Testing – the part most developers skip
Okay, confession time: I used to write extensions without any tests. It worked… until a MediaWiki core upgrade broke my hook signatures. Then I discovered Extension:Test and the phpunit harness that ships with MediaWiki.
Writing a simple hook test
class MyExtensionHooksTest extends MediaWikiTestCase {
public function testParserFunctionRegistration() {
$parser = new Parser();
$parser->firstCallInit();
$this->assertTrue( $parser->getFunctionHook( 'myfunc' ) !== null );
}
}
Notice the use of MediaWikiTestCase – it boots a minimal wiki environment, so you don’t have to mock everything yourself. If you’re feeling fancy, you can spin up a TestUser and assert permission checks too.
Real‑world example: a “smart” infobox extension
Imagine you want an infobox that pulls data from an external API, caches it, and lets privileged users override fields. Here’s a sketch of the architecture:
- Parser function
#smartinfobox– registers the markup. - Service
MyExtension\Infobox\Fetcher– handles API calls and caching. - Hook
ParserAfterParse– injects the rendered HTML. - Permission
myext-infobox-edit– gates the override UI.
Putting it together in extension.json:
{
"name": "SmartInfobox",
"services": {
"SmartInfobox.Fetcher": {
"class": "SmartInfobox\\Fetcher",
"arguments": [ "@MainWANObjectCache", "@HttpRequestFactory" ]
}
},
"Hooks": {
"ParserFirstCallInit": "SmartInfobox\\Hooks::registerParserFunction",
"ParserAfterParse": "SmartInfobox\\Hooks::injectInfobox"
},
"Permissions": {
"myext-infobox-edit": {
"desc": "Edit SmartInfobox overrides",
"right": "myext-infobox-edit"
}
}
}
And a quick look at the fetcher:
class Fetcher {
private $cache;
private $http;
public function __construct( WANObjectCache $cache, HttpRequestFactory $http ) {
$this->cache = $cache;
$this->http = $http;
}
public function getData( $id ) {
$key = $this->cache->makeKey( 'smartinfobox', 'data', $id );
return $this->cache->getWithSetCallback(
$key,
$this->cache::TTL_DAY,
function () use ( $id ) {
$req = $this->http->createRequest( "https://api.example.com/item/$id" );
$resp = $req->execute();
return json_decode( $resp->getBody(), true );
}
);
}
}
That’s it. A few lines, a service, a cache, and you’ve got a robust, testable component. The rest – rendering the HTML, handling overrides – follows the same patterns we discussed earlier.
Wrapping up (but not really)
If you’ve made it this far, you probably already feel the weight of “advanced” in the title. The truth is, mastery isn’t a single checklist; it’s a habit of questioning every assumption: “Do I really need a global variable here?” “What happens if the cache server goes down?” “Can I write a unit test for that edge case?”
So, next time you start a new extension, try to:
- Map out the hook flow before you write a line of code.
- Define services in
extension.jsonfrom day one. - Wrap any expensive work in
ObjectCacheorDeferredUpdates. - Give each permission a clear description and a short name.
- Write at least one test – even if it’s just a sanity check.
And remember: the MediaWiki community is full of folks who have wrestled with the same quirks. A quick search on the talk page or a ping on the #mediawiki-dev IRC channel can save you hours of head‑scratching.
Happy hacking, and may your hooks never collide.