Mastering MediaWiki Hooks for Extension Development

Why Hooks Matter in MediaWiki Extension Development

Imagine you’ve built a tiny extension that logs every page view. You could sprinkle a wfDebug() call everywhere, but that’s messy, hard to maintain and basically spaghetti code. MediaWiki’s hook system is the clean‑cut alternative: it lets you inject custom logic at well‑defined points without touching core files.

Hooks aren’t a new concept – they’ve been there since the early days of MediaWiki – but they’ve evolved. Starting with 1.35 the framework encourages hook handlers registered in extension.json, and from 1.44 onward domain events are creeping in as the preferred “post‑change” notification. Knowing the difference between a void hook (just runs) and an abortable hook (can stop the operation) is the first step to mastering the system.

Getting Your Hands Dirty: Registering a Hook

All registration lives in extension.json. A minimal snippet looks like this:

{
    "Hooks": {
        "EditFilter": "MyExtension\\Hooks::onEditFilter",
        "MyCustomHook": "MyExtension\\Hooks::onMyCustomHook"
    }
}

Notice two things:

  • The key is the literal hook name as MediaWiki defines it.
  • The value is a fully‑qualified PHP static method reference – you can also point to a global function if you prefer.

Once that file is in place, MediaWiki will automatically load the class (or function) when the hook fires. No need to litter $wgHooks assignments in LocalSettings.php, although you still can for quick experiments.

Writing the Handler – What It Looks Like

A handler receives whatever arguments the core hook passes. For EditFilter the signature is:

public static function onEditFilter( $editPage, &$text, &$summary, $user, &$minoredit ) {
    // Your logic here.
    // Return true to let the edit continue, false to abort.
}

There are a few quirks to keep in mind:

  1. Reference parameters (the & symbols) mean you can modify the value in‑place – perfect for sanitising user input.
  2. Returning false from an abortable hook stops the core flow and shows the error message you return via $editPage->addMessage().
  3. If you simply return true; (or omit a return, which defaults to true) the edit proceeds untouched.

Some hooks, like PageSaveComplete, are void. They don’t care about your return value – you just do something after the page has been saved, such as push a notification to an external service.

HookRunner, HookContainer, and the New HookHandlers

Under the hood the core uses a HookRunner object. When you call $this->getHookRunner()->onArticlePurge( $this ) (see the WikiPage::doPurge example on the MediaWiki site) the runner looks up all handlers for ArticlePurge and executes them in order.

Since 1.35 you can register the same hook via a HookHandler class instead of a static method. Here’s a quick illustration:

{
    "Hooks": {
        "PageContentSave": "MyExtension\\Hooks\\PageSaveHandler"
    }
}
namespace MyExtension\\Hooks;

use MediaWiki\\Hook\\PageContentSaveHook;

class PageSaveHandler implements PageContentSaveHook {
    /** @inheritDoc */
    public function onPageContentSave( $wikiPage, $content, $user, $summary, $isMinor, $isWatch, $section, $flags, $revision ) {
        // Do something clever.
        return true;
    }
}

Why bother? HookHandler classes let you type‑hint the exact hook interface, giving you autocompletion and static analysis – a small win for maintainability.

Abortable vs. Void – A Quick Reference

  • Abortable: EditFilter, UploadForm::BeforeProcessing, UserRenameWillRename. Returning false halts the operation.
  • Void: PageSaveComplete, RecentChangeSave, ParserAfterParse. Return value ignored.

Domain Events – The Future of Post‑Change Hooking

Starting with MediaWiki 1.44, domain events are the recommended way to react after something has happened. They’re basically the same idea as void hooks but live in a separate event‑dispatcher system that can be consumed by services outside PHP (e.g. Python bots pulling from a message queue).

Take the PageLatestRevisionChanged event introduced in 1.45. Instead of listening to PageSaveComplete you subscribe to the event:

use MediaWiki\\Event\\PageLatestRevisionChangedEvent;

function onPageLatestRevisionChanged( PageLatestRevisionChangedEvent $event ) {
    $title = $event->getTitle();
    // Push to external analytics.
}

When the event system is fully fleshed out you’ll see fewer “post‑save” hooks floating around, but the classic hook API will remain for decades – it’s not going anywhere overnight.

Best Practices (And a Few Gotchas)

Below is a short cheat‑sheet that I’ve gathered from years of tinkering:

  • Keep handlers small. A 20‑line function is easier to test than a 200‑line monolith.
  • Never exit or die. Throw an exception only if you really need to abort the whole request.
  • Beware of order. Handlers run in the order they appear in extension.json. If you need to guarantee execution after all others, use the “After” variant of the hook (e.g. ParserAfterParse).
  • Watch for reference parameters. Accidentally overwriting a reference can break unrelated extensions.
  • Test with the right MediaWiki version. Hooks can change signature between releases – the MediaWiki docs keep a version‑specific table.

One pitfall I ran into early on was assuming that every hook could be registered globally. In reality, some hooks are only available when a particular feature flag is enabled – otherwise the hook simply never fires. Check the hook documentation for a “Available since” note.

Putting It All Together – A Mini‑Extension Example

Let’s walk through a quick example that logs every successful edit to a custom table. The code is intentionally compact, focusing on the hook mechanics rather than schema design.

{
    "name": "EditLogger",
    "author": "Your Name",
    "url": "https://example.org/EditLogger",
    "version": "1.0.0",
    "manifest_version": 2,
    "Hooks": {
        "EditFilter": "EditLogger\\Hooks::onEditFilter"
    },
    "AutoloadClasses": {
        "EditLogger\\Hooks": "includes/Hooks.php"
    }
}
namespace EditLogger;

class Hooks {
    /**
     * Log the edit if it passes all other filters.
     *
     * @param EditPage $editPage
     * @param string &$text (reference)
     * @param string &$summary (reference)
     * @param User $user
     * @param bool &$minoredit (reference)
     * @return bool
     */
    public static function onEditFilter( $editPage, &$text, &$summary, $user, &$minoredit ) {
        // Simple sanity check – skip bots.
        if ( $user->isBot() ) {
            return true;
        }

        // Insert into our logging table.
        $dbw = wfGetDB( DB_MASTER );
        $dbw->insert(
            'edit_logger',
            [
                'el_user' => $user->getId(),
                'el_page' => $editPage->getTitle()->getArticleID(),
                'el_timestamp' => $dbw->timestamp(),
                'el_summary' => $summary
            ],
            __METHOD__
        );

        // Let the edit continue.
        return true;
    }
}

Enable the extension via wfLoadExtension( 'EditLogger' ); in LocalSettings.php and you have a functional logger that only runs after all other EditFilter handlers have approved the edit.

Where to Find More Hooks

The MediaWiki documentation lists hooks in two convenient ways:

If you ever feel stuck, the HookRunner source is surprisingly readable. Skim through HookRunner.php to see how MediaWiki resolves the handler list and calls each callback.

Final Thoughts

Hooks are the glue that lets extensions play nicely with core MediaWiki. Mastering them means you can:

  1. Inject behaviour exactly where you need it.
  2. Respect other extensions by chaining handlers gracefully.
  3. Future‑proof code by moving toward HookHandlers and domain events.

Don’t underestimate the small things – a typo in a hook name, or forgetting to mark a parameter as a reference, can make the whole extension silently fail. The best way to learn is to experiment: pick a simple hook, write a handler, watch the logs, and iterate. Before you know it, you’ll be wielding MediaWiki’s extensibility like a seasoned carpenter.

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