Introduction to MediaWiki Hooks for Custom Extensions
In plain English, a hook is a tiny “call‑out” point baked into MediaWiki’s core
What are MediaWiki hooks?
In plain English, a hook is a tiny “call‑out” point baked into MediaWiki’s core. When the software reaches a certain moment—say a page is saved, a user logs in, or a parser starts its work—it fires the corresponding hook. Any extension that has registered a handler for that hook gets a chance to run its own code right then.
The magic is that the core never knows or cares what the handler does; it merely hands over the arguments and moves on. This decoupling is why the hook system has become the de‑facto way to extend MediaWiki without hacking core files.
Why bother with hooks?
- Non‑intrusive. You keep the core clean, which means easier upgrades.
- Composable. Multiple extensions can listen to the same hook; they run sequentially, each seeing the modifications made by its predecessor.
- Fine‑grained. From low‑level events like
ArticleDeleteto high‑level parser hooks, there’s practically a hook for every sensible action.
Hook anatomy
Every hook is identified by a string name—EditFilter, PageSaveComplete, ParserFirstCallInit, and so on. When MediaWiki reaches the relevant spot, it invokes the HookContainer::run() method, passing the name and the arguments that the hook expects.
From the extension’s perspective you only need two things:
- A registration entry in
extension.jsonthat maps the hook name to a PHP callback. - A handler method (static or instance) whose signature matches the hook’s specification.
Registering a hook
Here’s the minimal JSON you’d drop into your extension’s extension.json file:
{
"Hooks": {
"EditFilter": "MyExt\\Hooks::onEditFilter",
"MyCustomHook": "MyExt\\Hooks::handleCustom"
}
}
Notice the fully‑qualified class name; MediaWiki will autoload the class when the hook fires. You can also register callbacks that are plain functions, but using a namespaced class keeps things tidy.
Writing the handler
The handler receives the exact arguments the hook promises. For EditFilter the signature looks like this:
class MyExt\Hooks {
/**
* @param EditPage $editPage
* @param string &$text
* @param string &$summary
* @param User $user
* @param bool &$minoredit
* @return bool|void
*/
public static function onEditFilter( $editPage, &$text, &$summary, $user, &$minoredit ) {
// Example: block edits that contain the word “spammy”
if ( strpos( $text, 'spammy' ) !== false ) {
$editPage->addError( 'Your edit contains prohibited content.' );
return false; // abort the save
}
// No return means the edit proceeds
}
}
Two quirks to remember:
- Pass‑by‑reference arguments (the
&ones) let you mutate data that the core will later use. - If the hook is “abortable”, returning
falsestops the operation; otherwise you just let the function fall off the end.
Void vs. abortable hooks
MediaWiki classifies hooks into two buckets.
Void hooks are purely informational. They don’t affect the flow, so whatever you return is ignored. Typical examples are ArticleDeleteComplete or PageSaveComplete. You can use them to log activity, update external services, or trigger a cache purge.
Abortable hooks can short‑circuit the original action. If any handler returns false, MediaWiki halts the process and shows an error (if the handler set one). EditFilter, UploadVerifyFile and UserLoginForm fall in this category.
From old‑style registration to the new HookHandler system
Up through MediaWiki 1.34 most extensions added hooks in extension.json exactly as shown above. Starting with 1.35 the platform introduced a more structured approach: HookHandlers and interfaces. The idea is to declare an interface that mirrors the hook’s signature, then let the extension implement that interface. MediaWiki wires everything together automatically.
Why bother? Two reasons:
- Static analysis tools can now verify that your handler matches the hook.
- Future‑proofing. As the core evolves, the interface can be versioned without breaking existing code.
Example: Using a HookHandler
First, add the handler mapping in extension.json:
{
"HookHandlers": {
"EditFilter": "MyExt\\Hooks\\EditFilterHandler"
}
}
Then create the interface implementation:
namespace MyExt\Hooks;
use MediaWiki\Hook\EditFilterHook;
class EditFilterHandler implements EditFilterHook {
public function onEditFilter( $editPage, &$text, &$summary, $user, &$minoredit ) {
// same logic as before, just inside a class that implements the interface
if ( preg_match( '/\bspammy\b/i', $text ) ) {
$editPage->addError( 'Please remove the word “spammy”.' );
return false;
}
// returning void (i.e. nothing) lets the edit continue
}
}
Notice the use MediaWiki\Hook\EditFilterHook; import. The interface lives in includes/Hooks and carries the exact method signature.
Creating your own hook
Sometimes the core simply doesn’t fire a hook where you need one. No problem—extensions can define brand‑new hooks.
- Pick a unique name, preferably namespaced, e.g.
MyExtBeforeFooBar. - Document the argument list in a
.phpdocblock. - When the moment arrives in your code, call the hook runner:
$hookRunner = $this->getHookRunner();
$hookRunner->onMyExtBeforeFooBar( $someObject, $extraData );
Finally, register the new hook exactly as you would a core one—add an entry under Hooks (or HookHandlers) in extension.json. Other extensions can now listen to MyExtBeforeFooBar without you having to change a line of core code.
Practical tips and common pitfalls
- Don’t overload the handler. Keep it focused. If you find yourself doing database writes, cache clears, and API calls all in one method, split it up.
- Watch the return type. Accidentally returning
nullfrom an abortable hook can be interpreted as “continue”, but some developers treatnullas “failed”. Be explicit. - Order matters. Handlers run in the order they appear in
extension.json. If two extensions both modify the same text, the later one sees the earlier one's changes. - Testing. The hook system is easy to unit‑test: instantiate the
HookContainerand feed it mock arguments. - Performance. Each hook adds a tiny overhead. If you register a handler for a hot‑path hook like
ParserAfterParse, make sure the code inside is lean.
Hook‑related resources
MediaWiki’s official documentation lives at Manual:Hooks. The page lists every built‑in hook, grouped by function, and provides a short description of the arguments.
For the newer interface‑based system see the HookHandlers manual. It also explains how to migrate an old‑style extension to the new pattern.
If you’re interested in the future direction, keep an eye on Domain events. Starting with MediaWiki 1.44 many “after” hooks are being superseded by a more robust event system. For now, though, hooks remain the workhorse for most extension developers.
Putting it together – a minimal custom extension
Below is a compact example that demonstrates everything we’ve covered: a new hook that fires just before a page is rendered, plus a handler that injects a custom banner.
# extension.json
{
"name": "BannerInjector",
"author": "Jane Doe",
"version": "1.0.0",
"Hooks": {
"BeforePageDisplay": "BannerInjector\\Hooks::onBeforePageDisplay"
},
"AutoloadClasses": {
"BannerInjector\\Hooks": "includes/Hooks.php"
},
"manifest_version": 2
}
# includes/Hooks.php
namespace BannerInjector;
use OutputPage;
use Skin;
class Hooks {
public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
// Simple HTML banner, could be read from a config variable
$banner = '<div class="custom-banner">Welcome to our wiki!</div>';
$out->addHTML( $banner );
// No return needed – this is a void hook
}
}
Drop the folder into extensions/, add wfLoadExtension( 'BannerInjector' ); to LocalSettings.php, and you’ll see the banner appear on every page.
Conclusion
Hooks are the connective tissue that lets you bend MediaWiki to your will without touching its heart. Whether you’re tapping an existing hook, defining a brand‑new one, or migrating to the modern HookHandler interface, the steps are straightforward: register, implement, and let MediaWiki do the rest. A well‑placed hook can turn a simple wiki into a tailored platform that does exactly what your community needs.