Understanding MediaWiki's Hook System for Extension Development
The modern MediaWiki hook system: extension.json registration, handler classes, arguments and return values, ordering, and debugging hook execution.
Hooks are MediaWiki's event system for extension developers: a hook is a named point in the request lifecycle — PageSaveComplete, BeforePageDisplay, ParserAfterParse — where extensions register functions to run. Since MediaWiki 1.35 the mechanics are cleaner: registration in extension.json, handler classes with dependency injection, and no more global $wgHooks arrays in new code. This article is the modern pattern, plus the debugging that hooks eventually require.
1. The lifecycle in 2026
- An event happens in core (or another extension) — the code calls
$this->getHookContainer()->run( 'MyHookName', [ $arg1, $arg2 ] ); - HookRunner collects all registered handlers for that hook
- Handlers run in registration order; unless a handler returns
false(aborts the chain for that hook) orHookAbortErrors, all run
2. Registering handlers
Old way (still works, avoid in new code):
$wgHooks['BeforePageDisplay'][] = 'MyExtensionHooks::onBeforePageDisplay';
Modern way — declare in extension.json:
{
"name": "MyExtension",
"Hooks": {
"BeforePageDisplay": "MyExtension\\Hook\\Handler\\Main"
},
"HookHandlers": {
"MyExtension\\Hook\\Handler\\Main": {
"class": "MyExtension\\Hook\\Handler\\Main",
"services": [ "UserFactory", "OutputPage" ]
}
}
}
The handler class:
class Main implements BeforePageDisplayHook {
public function onBeforePageDisplay( $out, $skin ): void
{
// act on the page output
}
}
Interface-based handler signatures (BeforePageDisplayHook etc.) give autocompletion and type safety, and the services array injects exactly the services the class needs — the dependency injection that replaced global accessors. Since MediaWiki 1.45/wfGlobalName era: the surviving globals ($wgUser, etc.) are deprecated paths; inject services instead.
3. Arguments and return values
Handlers receive the hook's arguments (mostly by reference, so they can modify state) and can:
- Return
void/true— continue normally - Return
false— stop other handlers for this hook (marking 'handled') - Throw — abort the operation, in hooks designed for it
The documented contract per hook (on Manual:Hooks) states which of these is meaningful — most hooks ignore false; some (like PageSaveComplete) are notifications that must not block.
4. Ordering inside the chain
MediaWiki offers no per-handler priority control in the modern registration; order is determined by extension load order (alphabetical) and, for legacy $wgHooks entries, insertion time. Practical consequences:
- Rename your extension to order it — a blunt but used trick; do not rely on it
- Hook into later points — if you must run after other extensions,
BeforePageDisplayruns after most content hooks; document your expected position - Compose, don't fight — whenever two extensions fight over the same output, prefer one canonical handler that coordinates
5. Performance rules
- Handlers run on every occurrence — a
ParserAfterParsehandler runs for every parsed page; keep work lazy and cache results (WANObjectCache) - Registration must be cheap: no heavy setup at load time — services instantiate lazily by design
- Use the right hook:
ParserFirstCallInitfor registering parser functions is once-per-process, whileParserAfterParseis per-parse — placing logic in the wrong one is the classic slowdown
6. Debugging hooks
Three tools for 'my hook doesn't run / runs twice':
Special:Version— confirm the extension is loaded (a missing entry means registration failed early)- Log or error-log in the handler — ‘does it fire at all’ splits the problem in two immediately
- The profiler (
$wgProfiler) —Hook-Runner::run(MyHook)entries show execution counts and costs per hook
The Manual:Hooks page is the index of every hook with signatures and examples — bookmark it before writing handlers, and the development manual covers the full extension lifecycle.