Understanding MediaWiki's Hook System for Extension Development
Understanding MediaWiki’s Hook System for Extension Development
When you first peek at MediaWiki’s source you’ll notice a curious pattern: scattered Hooks::run calls, each paired with a string like ArticleSaveComplete or UserLoginComplete. Those are the beating heart of the extension ecosystem. In plain English, a “hook” is a named spot in the core (or in another extension) where custom code can jump in, run its own logic, and then gracefully hand the reins back.
Why Hooks Matter
- Decoupling. Core developers keep the web‑app tidy; extensions add features without touching the core files.
- Flexibility. Anything from a tiny label change to a full‑blown workflow can be attached to the same event.
- Stability. Since extensions only react to well‑documented events, upgrades tend to be less painful.
Think of MediaWiki as a train station. The platform is the core, the timetables are the hooks, and each extension is a passenger stepping onto the train exactly when it needs to.
Hook Anatomy
A typical hook registration looks like this, buried somewhere inside includes/Article.php:
Hooks::run( 'ArticleSaveComplete', [ $article, $user, $text, $summary, $isMinor, $isWatch, $section ] );Four things happen here:
- The static
Hooks::runmethod is called. - The first argument is the hook name (a string).
- The second argument is an indexed array of parameters that will be passed to callbacks.
- The function loops through every registered handler, invoking them in the order they were added.
If you’re wondering whether the callback can stop the original operation, the answer is “yes, but only if the hook’s documentation says so.” Some hooks are “abortable” – returning false tells MediaWiki to cancel the default flow.
Registering a Handler
All the heavy lifting happens inside the extension’s extension.json (the modern, preferred method) or the older extensionName.php file. Here’s a minimal extension.json that wires up a handler for ArticleSaveComplete:
{
"name": "MyFirstHook",
"author": "Jane Doe",
"version": "1.0",
"hooks": {
"ArticleSaveComplete": "MyFirstHook\\Hooks::onArticleSaveComplete"
},
"AutoloadClasses": {
"MyFirstHook\\Hooks": "includes/Hooks.php"
}
}Notice the tidy JSON map: hook name → fully‑qualified PHP method. MediaWiki will autoload the class once the hook fires.
The Callback Signature
Every hook has its own expected signature. Miss a parameter or get the order wrong, and you’ll get cryptic “ArgumentCountError” messages. For ArticleSaveComplete, the callback should look like this:
namespace MyFirstHook;
class Hooks {
public static function onArticleSaveComplete( $article, $user, $text, $summary, $isMinor, $isWatch, $section ) {
// Your custom logic goes here.
// Returning true (or nothing) tells MediaWiki to continue.
return true;
}
}Here’s a quick tip: If you only need a few of the parameters, just leave the rest out – PHP will still feed them to the function, but you’re not forced to name them.
Aborting an Action
Some hooks, like UserLoginFormSubmit, allow you to halt the process. The convention is to return false after adding an error to the output. For example:
public static function onUserLoginFormSubmit( &$user, &$password, &$retval ) {
if ( strpos( $user->getName(), 'bot' ) !== false ) {
$retval->addError( 'Bots are not allowed to log in directly.' );
return false; // abort login
}
return true;
}Be careful with abortable hooks – returning false when the documentation says “ignore return value” can break core functionality.
Hook Types at a Glance
MediaWiki ships with over 250 hooks, but they cluster into a few logical families:
- Parser hooks. Run while wikitext is turned into HTML (e.g.,
ParserBeforeStrip). - Page lifecycle hooks. Trigger on edit, delete, view – the usual suspects.
- User‑related hooks. Login, logout, user creation, rights changes.
- Special page hooks. Hooks that fire when a particular Special: page is rendered.
- Extension‑specific hooks. Many extensions expose their own events (e.g.,
EchoAddAlertfrom the Echo extension).
If you’re hunting for a hook, the best place is the MediaWiki Manual:Hooks page – it’s a searchable table with brief descriptions and links to the source file where the hook lives.
Best Practices (and Pitfalls)
Writing a hook is easy; writing a good hook takes a bit more thought.
1. Keep it light
Hooks run synchronously. A heavy database query inside ArticleSaveComplete will slow down every single edit. If you need expensive work, consider queuing a job (JobQueue) instead.
2. Respect the contract
A handful of hooks intentionally pass objects by reference (e.g., &$output in OutputPageBeforeHTML). Mutating those objects is the whole point, but don’t change unrelated properties – it can cause subtle bugs in downstream handlers.
3. Guard against recursion
Some extensions fire their own hooks which other extensions may listen to. If your callback triggers the same event again (perhaps by programmatically saving a page), you’ll end up in an infinite loop. Use a static flag or the Hooks::run return value to break the cycle.
4. Document your handler
Because many extensions coexist on a single wiki, a vague comment like “// do something” is unhelpful. State the hook name, what you modify, and any side‑effects. Future maintainers (or even you a month later) will thank you.
5. Test in isolation
MediaWiki’s core test suite includes phpunit tests that invoke hooks directly. Write a small unit test that calls Hooks::run with a mock object and asserts the expected outcome. It’s more reliable than testing through a UI edit.
Real‑World Example: Adding a Custom Badge
Suppose you want to give users a “First Edit” badge the moment they save their inaugural page. The workflow is:
- Listen to
ArticleSaveComplete. - Check how many edits the user has made.
- If it’s the first, insert a row into the
user_badgestable.
Here’s a concise implementation:
namespace FirstEditBadge;
class Hooks {
public static function onArticleSaveComplete( $article, $user ) {
// Quick early exit if user is an IP or already has the badge.
if ( $user->isAnon() ) {
return true;
}
$dbr = wfGetDB( DB_REPLICA );
$editCount = $dbr->selectField(
'revision',
'COUNT(*)',
[ 'rev_user' => $user->getId() ],
__METHOD__
);
if ( $editCount == 1 ) { // this edit is the first
$dbw = wfGetDB( DB_MASTER );
$dbw->insert(
'user_badges',
[ 'ub_user' => $user->getId(), 'ub_badge' => 'first-edit', 'ub_timestamp' => $dbw->timestamp() ],
__METHOD__
);
}
return true;
}
}Notice the guard clauses – they keep the hook cheap for the vast majority of edits (i.e., when the user already has more than one edit). Also, the method uses MediaWiki’s database abstraction layer to stay compatible across DB engines.
Advanced Topics
Dynamic Hook Registration
Sometimes you need to register a hook only under certain conditions (e.g., a feature flag). You can do this at runtime via Hooks::register:
if ( $wgEnableMyFeature ) {
Hooks::register( 'BeforePageDisplay', 'MyFeature\\Hooks::onBeforePageDisplay' );
}This approach works best in the extension’s extension.json Hooks section when the flag is static, but for truly dynamic scenarios, the above code is fine.
Hook Priority
MediaWiki doesn’t have a built‑in priority system, but you can simulate it by controlling registration order. Extensions are loaded alphabetically by default, but you can influence it using the weight key in extension.json. A lower weight means “load earlier,” which translates to “run earlier” for its hooks.
Hook Contexts
Some hooks provide a “context object” that bundles several related services (e.g., \\MediaWiki\\Context\\IContextSource). If you’re writing a new hook, consider passing a context rather than a grab‑bag of individual parameters. It makes future extensions easier to maintain.
Creating Your Own Hook
If you’re developing a large custom extension and you find yourself repeatedly needing to expose an event, you can add a new hook. The steps are straightforward:
- Pick a unique name (e.g.,
MyExtensionAfterSomething). - In the spot where you want the event, call
Hooks::runwith the appropriate arguments. - Add an entry to your
documentation(a simple table on the extension’s wiki page). - Optionally, write a unit test that verifies the hook fires.
Make sure the name follows the existing naming convention – typically “ThingVerb” – so that other developers can guess its purpose.
Conclusion
The hook system is MediaWiki’s secret sauce, turning a monolithic wiki engine into a vibrant ecosystem of add‑ons. By understanding where hooks live, how they’re registered, and what contracts they obey, you gain the power to weave custom behavior into virtually any part of the platform. Remember to keep your callbacks lightweight, respect the documented signatures, and test them thoroughly. With those habits, you’ll create extensions that coexist peacefully, survive upgrades, and most importantly – do exactly what you need them to do without stepping on each other’s toes.