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:

  1. The static Hooks::run method is called.
  2. The first argument is the hook name (a string).
  3. The second argument is an indexed array of parameters that will be passed to callbacks.
  4. 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., EchoAddAlert from 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:

  1. Listen to ArticleSaveComplete.
  2. Check how many edits the user has made.
  3. If it’s the first, insert a row into the user_badges table.

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:

  1. Pick a unique name (e.g., MyExtensionAfterSomething).
  2. In the spot where you want the event, call Hooks::run with the appropriate arguments.
  3. Add an entry to your documentation (a simple table on the extension’s wiki page).
  4. 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.

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