Mastering MediaWiki Hooks for Extension Developers

Why Hooks Matter More Than You Think

Picture this: you’re tinkering with a fresh‑off‑the‑shelf MediaWiki extension, and suddenly you need to inject a tiny piece of logic right after a user saves a page. You could hack core files—yeah, not recommended—but there’s a cleaner, more elegant lever in the toolbox: hooks.

Hooks are the secret sauce that lets extensions whisper to the core without shouting. They’re the reason you can add a “thank‑you” banner after an edit, log a custom event, or even veto a page move based on a corporate policy—all without touching the core’s fragile innards.

Hook Basics, Minus the Boredom

  • What’s a hook? Think of it as a named event that MediaWiki fires at a predictable point.
  • When does it fire? Right before or after a specific action—like PageSaveComplete after a save.
  • Who catches it? Any extension that registers a callback for that hook name.

Sounds simple, right? In practice you’ll see a mix of “do something” and “don’t do something” hooks. The former are your “post‑action” hooks (e.g., ArticleDeleteComplete), the latter are “pre‑action” (e.g., AbortMove).

Registering a Hook: The Nitty‑Gritty

First, you need to tell MediaWiki you’re interested. Open your extension’s extension.json and drop a Hooks section in there. No fancy XML, just plain JSON.


{
    "name": "MyAwesomeExtension",
    "author": "Jane Doe",
    "version": "1.0.0",
    "Hooks": {
        "PageSaveComplete": "MyAwesomeExtension\\Hooks::onPageSaveComplete",
        "AbortMove": "MyAwesomeExtension\\Hooks::onAbortMove"
    }
}

Notice the double backslashes? That’s PHP’s namespace separator, and it looks a bit odd the first time you type it—don’t worry, it’s perfectly legit.

Writing the Callback: A Quick Dive

Now, create the class referenced above. Keep it in includes/Hooks.php (or wherever you like). The method signature must match what MediaWiki expects; otherwise you’ll get a cryptic “hook handler returned false” error.


// includes/Hooks.php
namespace MyAwesomeExtension;

class Hooks {
    /**
     * Called after a page is saved.
     *
     * @param WikiPage $wikiPage The page object
     * @param User $user The user who performed the save
     * @param string $summary Edit summary
     * @param bool $isMinor Whether the edit was marked minor
     * @param null $isWatch
     * @param $section
     * @param int $flags
     * @param RevisionRecord $revisionRecord
     * @return bool Must return true to continue processing
     */
    public static function onPageSaveComplete( $wikiPage, $user, $summary,
        $isMinor, $isWatch, $section, $flags, $revisionRecord ) {

        // A tiny thank‑you note for the editor
        $msg = "Thanks, {$user->getName()}, for your contribution!";
        $wikiPage->getTitle()->getTalkPage()->addComment( $msg );

        // Log it for fun
        wfDebugLog( 'MyAwesomeExtension', "Page saved: {$wikiPage->getTitle()->getPrefixedText()}" );

        // Return true so other hooks keep running
        return true;
    }

    /**
     * Prevent moving pages out of the "Help" namespace.
     *
     * @param Title $oldTitle
     * @param Title $newTitle
     * @param User $user
     * @param string &$error
     * @return bool Return false to abort the move
     */
    public static function onAbortMove( $oldTitle, $newTitle, $user, &$error ) {
        if ( $oldTitle->getNamespace() === NS_HELP ) {
            $error = "Moving Help pages is disabled for now.";
            return false; // abort the move
        }
        return true;
    }
}

That snippet does two things: it drops a friendly note on the talk page after every edit, and it blocks moving any page in the Help namespace. Both are common patterns you’ll see across the ecosystem.

When Hooks Misbehave (Or When You Mis‑hook)

Ever seen a page just freeze after an edit? That’s often a rogue hook returning false unintentionally. The MediaWiki core treats a false return as “stop further processing.” So, unless you truly want to abort, always end with return true;. It’s a tiny detail that trips up many newbies.

Another gotcha: the order of hook execution. MediaWiki runs hooks in the order they were registered, which means your extension could be stepping on someone else’s toes if you both listen to PageSaveComplete. You can tweak the order by using the HookHandler service in newer MediaWiki versions, but that’s an advanced topic—just keep it in mind.

Debugging Hooks Without Losing Your Mind

One of the best ways to see what’s happening is to sprinkle wfDebugLog calls in your callbacks. They’ll write to debug.log (or wherever your $wgDebugLogFile points). Example:

wfDebugLog( 'MyAwesomeExtension', "Entering onPageSaveComplete for {$wikiPage->getTitle()}" );

If you’re on a shared hosting environment where you can’t read the log directly, just fire a small error_log() to the PHP error stream; it usually ends up in the web server’s error log.

Performance: Hooks Are Not Free

Every hook you add introduces a tiny overhead. In a busy wiki, a hook that runs a heavyweight SQL query on every page view can become a bottleneck. The rule of thumb? Keep it lean. If you need heavy lifting, defer it to a background job using the JobQueue system.

For example, instead of sending an email right inside PageSaveComplete, push a job:

$job = new MyEmailJob( $wikiPage->getTitle(), [ 'user' => $user->getId() ] );
$job->insert(); // queues it for later processing

That way the user gets a snappy response, and the email goes out when the queue worker is ready.

Real‑World Hook Use Cases (A Few I’ve Seen)

  1. Custom Spam Filter – Hook into ArticleSave to run a regex check on the wikitext before it lands.
  2. Revision Tagging – Use RevisionInsertComplete to add a custom tag that shows up in the history.
  3. Dynamic Page Templates – Listen to GetPageTemplate (if you’ve got the TemplateData extension) to swap in a different template based on user groups.
  4. Audit Trail – Log every DeleteComplete event to an external syslog server for compliance.

Those examples illustrate how hooks let you extend the wiki’s behavior without reinventing the wheel each time.

Testing Hooks: The “Don’t Break the Ship” Approach

If you’re serious about shipping a stable extension, write a PHPUnit test for each hook. MediaWiki ships with a HooksTest.php base class you can extend.


class MyHooksTest extends MediaWikiIntegrationTestCase {
    public function testOnPageSaveCompleteAddsTalkComment() {
        $title = Title::newFromText( 'TestPage' );
        $page = WikiPage::factory( $title );
        $user = $this->getTestUser()->getUser();

        // Simulate a save
        $revision = $page->doUserEditContent(
            ContentHandler::makeContent( 'Hello world', $title ),
            $user,
            'Test edit'
        );

        // Trigger the hook manually
        Hooks::onPageSaveComplete( $page, $user, 'Test edit', false, false, '', 0, $revision );

        // Check the talk page for our comment
        $talk = $title->getTalkPage();
        $this->assertStringContainsString(
            "Thanks, {$user->getName()}, for your contribution!",
            $talk->getContent()->getText()
        );
    }
}

Running phpunit will now verify your hook does exactly what you expect. It’s a little extra work, but it saves you from a nasty surprise on production day.

Hook Evolution: From Legacy to Modern

Older extensions still use the Hooks::register static method in extension.php. Newer code prefers the JSON‑based registration because it’s declarative and plays nicely with MediaWiki’s extension loader. If you’re maintaining legacy code, you might see something like:


$wgHooks['PageSaveComplete'][] = 'MyLegacyExtension::onPageSaveComplete';

Both approaches work, but the JSON style gives you a clear, one‑stop view of all your hooks in the extension.json file. If you’re starting fresh, stick with JSON.

Common Mistakes (And How to Avoid Them)

  • Forgetting the return value. Returning null is the same as false for most hooks—your code will silently abort the chain.
  • Using global variables. Hooks run in a sandboxed context; relying on globals can cause hard‑to‑track bugs.
  • Hard‑coding namespace IDs. Use the NS_ constants (e.g., NS_TALK) instead of raw numbers.
  • Running heavy DB queries. Cache results or defer to a job queue.

Putting It All Together: A Mini‑Extension Walkthrough

Let’s build a tiny “WelcomeBanner” extension that shows a custom banner to first‑time editors on their very first edit. We’ll need two hooks:

  1. PageSaveComplete – to flag the user as “has edited”.
  2. BeforePageDisplay – to inject the banner if the flag is missing.

Here’s the extension.json:


{
    "name": "WelcomeBanner",
    "author": "Alex Smith",
    "version": "0.1",
    "Hooks": {
        "PageSaveComplete": "WelcomeBanner\\Hooks::onPageSaveComplete",
        "BeforePageDisplay": "WelcomeBanner\\Hooks::onBeforePageDisplay"
    },
    "ResourceModules": {
        "ext.welcomeBanner": {
            "scripts": "modules/ext.welcomeBanner.js",
            "styles": "modules/ext.welcomeBanner.css"
        }
    }
}

Now the PHP callbacks:


namespace WelcomeBanner;

class Hooks {
    public static function onPageSaveComplete( $wikiPage, $user, $summary,
        $isMinor, $isWatch, $section, $flags, $revisionRecord ) {

        // Mark the user as having edited at least once
        $user->setOption( 'welcomeBannerSeen', 1 );
        $user->saveSettings();
        return true;
    }

    public static function onBeforePageDisplay( $out, $skin ) {
        $user = $out->getUser();
        if ( !$user->getOption( 'welcomeBannerSeen' ) ) {
            // Add our module which will render the banner
            $out->addModules( 'ext.welcomeBanner' );
        }
        return true;
    }
}

The JavaScript (kept simple) just injects a div:


// modules/ext.welcomeBanner.js
mw.loader.using( 'mediawiki.util' ).then( () => {
    const banner = document.createElement( 'div' );
    banner.className = 'welcome-banner';
    banner.textContent = 'Welcome to editing! 🎉';
    document.body.prepend( banner );
} );

And a tiny CSS file to make it look decent:


.welcome-banner {
    background:#fffae6;
    border:1px solid #ffd42a;
    padding:10px;
    text-align:center;
    font-weight:bold;
}

Deploy the extension, clear the cache, and you’ll see a bright banner for brand‑new editors. Once they make their first edit, the flag flips, and the banner disappears—just the way we want it.

Looking Ahead: Hooks in the Next MediaWiki Release

MediaWiki 1.43 is slated to introduce “hook priorities,” letting extensions declare whether they want to run before or after other handlers. That’ll give developers finer control, especially in crowded wikis where dozens of extensions vie for the same hook.

Until then, the best practice remains: document your hooks, keep them lightweight, and test them thoroughly. A well‑behaved hook can make your extension feel like a natural extension of the core; a mis‑behaving one can bring the whole wiki to a grinding halt.

Final Nuggets (Not a Conclusion, Just a Thought)

If you ever feel lost staring at a stack trace that mentions Hooks.php line 42, remember: you’re not alone. The MediaWiki community has a thriving Hooks documentation page and a lively Phabricator forum. Drop a question, share your snippet, and someone will probably point out that you missed a return true.

In the end, mastering hooks is less about memorizing every hook name and more about developing a mindset: “When something happens, what could I intervene with?” That curiosity will keep you pushing the boundaries of what MediaWiki can do, one hook at a time.

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