Best Practices for Developing Custom MediaWiki Hooks

If you’ve ever scratched your head over why a simple page‑save sometimes triggers a cascade of actions, you’ve already bumped into the world of hooks

Why Hooks Matter in MediaWiki

If you’ve ever scratched your head over why a simple page‑save sometimes triggers a cascade of actions, you’ve already bumped into the world of hooks. They’re the quiet backstage crew that let extensions whisper into the core’s ear at just the right moment. A well‑written hook can, for example, stamp a custom template onto new articles, fire off a notification, or even block a rogue edit before it lands.

Getting Started: Registering Your Hook

First things first—your extension’s extension.json is the place to make it official. Forget the old‑school registerHook calls; the JSON schema is the current gold standard (see the “Best practices for extensions” page for details).

{
    "name": "MyAwesomeHook",
    "author": "Jane Doe",
    "version": "1.0.0",
    "hooks": {
        "PageSaveComplete": "MyAwesomeHook\\Hooks::onPageSaveComplete"
    }
}

Notice the use of a namespaced class—this isn’t just for show. Namespaces keep the global namespace clean, and they’re a lifesaver when you later combine several extensions in one wiki.

Tip: Keep the Hook List Tight

  • Only register hooks you actually need; extraneous entries add noise.
  • Group related hooks under a common logical name; makes future maintenance smoother.
  • Document each hook’s purpose directly in extension.json with a short comment.

Implementing the Callback

The callback itself lives in a class file—usually under src/Hooks.php. Below is a skeleton that follows the MediaWiki standard of returning true to let the process continue.

<?php
namespace MyAwesomeHook;

use MediaWiki\HookContainer\HookContainer;
use Title;
use User;

class Hooks {
    /**
     * Called after a page has been saved.
     *
     * @param WikiPage $wikiPage
     * @param User $user
     * @param string $content
     * @param string $summary
     * @param bool $isMinor
     * @param null $isWatch
     * @return bool
     */
    public static function onPageSaveComplete( $wikiPage, $user, $content, $summary,
        $isMinor, $isWatch ) {
        // Your logic goes here.
        // For example, log a message if the page lives in a special namespace.
        if ( $wikiPage->getTitle()->inNamespace( NS_TALK ) ) {
            wfLogToFile( "Talk page saved by {$user->getName()}" );
        }
        // Always return true unless you deliberately want to abort the save.
        return true;
    }
}

Few things to keep in mind:

  1. Type hinting matters. Even though MediaWiki still supports a few loosely‑typed calls, adding proper type hints helps static analysers and future‑proofs your code.
  2. Avoid heavy work. Hooks run in the request cycle; a slow database query here will make the whole save feel sluggish.
  3. Never throw uncaught exceptions. If something goes sideways, log it and return true so the user isn’t greeted with a fatal error page.

Performance: Keep It Light

Performance is the silent killer of “good” hooks. Here are some quick hacks that sometimes get shoved under the rug.

  • Lazy loading. Only instantiate heavy objects (like external API clients) when you really them.
  • Cache results. Short‑lived caches (e.g., $wgMemc) are fine for data that doesn’t change every millisecond.
  • Batch DB queries. If you need to check multiple pages, bundle them into a single SELECT rather than looping.

For example, instead of this:

foreach ( $titles as $title ) {
    $page = WikiPage::factory( $title );
    $page->getContent();
}

Prefer a single query:

$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select(
    'page',
    [ 'page_id', 'page_title' ],
    [ 'page_title' => $titles ],
    __METHOD__
);

Testing Your Hook

When you finally think the code is ready, don’t just press “refresh”. Unit tests are the safety net that keeps your extension from breaking after a MediaWiki core update.

MediaWiki ships with PHPUnit integration. A minimal test looks like this:

<?php
class MyAwesomeHookTest extends MediaWikiTestCase {
    public function testTalkPageSaveLogging() {
        $title = Title::newFromText( 'Talk:Sandbox' );
        $page = WikiPage::factory( $title );
        $user = User::newFromName( 'TestUser' );

        // Simulate a save
        Hooks::onPageSaveComplete( $page, $user, 'Test content', 'Test summary', false, null );

        // Check the log file (or better, mock wfLogToFile)
        $this->assertStringContainsString(
            'Talk page saved by TestUser',
            file_get_contents( '/path/to/logfile.log' )
        );
    }
}

Two notes:

  1. Prefer MockObjects over real file writes; otherwise tests become flaky.
  2. Run the suite with phpunit --group MyAwesomeHook to isolate it from other extensions.

Security Considerations

Security is rarely an afterthought in hook development, but it should be baked in from day one. The following checklist often helps:

  • Validate input. Anything coming from the request—titles, user‑provided text, URLs—needs sanitisation.
  • Respect permissions. Use $user->isAllowed( 'edit' ) or similar checks before performing privileged actions.
  • Avoid direct database writes. Stick to MediaWiki’s WikiPage API; it ensures revisions and hooks fire correctly.

One easy mistake is logging raw user input. That can open the door to log injection attacks. Always run Sanitizer::stripAllTags (or a similar whitelisting approach) before writing to logs.

Documentation: The Unsung Hero

Never underestimate the power of a good README. In the context of hooks, the documentation should answer three questions:

  1. When does the hook fire? (e.g., after a page is saved, before an edit is parsed)
  2. What parameters are passed? (list types and a brief description)
  3. What should the callback return? (usually true to continue, false to abort)

Here’s a tiny excerpt you might paste into your README.md:

## Hook: PageSaveComplete
**When:** Right after a page is successfully written to the database.  
**Parameters:**
- `WikiPage $wikiPage` – The page object.
- `User $user` – The editor.
- `string $content` – Raw wikitext.
- `string $summary` – Edit summary.
- `bool $Minor` – Minor edit flag.
- `null|bool $isWatch` – Watchlist flag.

**Return:** `true` to let MediaWiki continue, `false` to abort the save.

Even a short block like this can save a future maintainer (or your future self) a lot of head‑scratching.

Versioning and Compatibility

MediaWiki evolves, and hook signatures can shift. Stick to the “semantic versioning” approach recommended on the MediaWiki extension pagep>

  • Major bump when you drop support for a MediaWiki version.
  • Minor bump when you add a new hook or feature.
  • Patch bump for bug‑fixes only.

Also, guard your code with if ( MediaWikiServices::getInstance()->getVersion() >= '1.38.0' ) checks if you need to call a newer function.

Common Pitfalls (and How to Dodge Them)

After months of trial, a handful of gotchas keep surfacing. Below is a quick “watch‑out” list.

  • Returning the wrong type. A stray return; (i.e., no value) will be interpreted as null, which MediaWiki treats like false and aborts the operation.
  • Modifying globals. Changing $wgUser or other globals inside a hook can create hard‑to‑track side effects.
  • Duplicate registrations. Register the same hook in both extension.json and LocalSettings.php; you’ll get double execution.
  • Assuming $wikiPage is always new. In PageContentSave you can get an existing page object; don’t treat it as a fresh draft.

Putting It All Together

So, what does a “best‑practice” hook look like in the wild? Imagine you need to auto‑assign a category to every new article in the Project namespace. Here’s the full combo—registration, callback, a tiny cache, and a test—all wrapped in the conventions described above.

{
    "name": "AutoProjectCategory",
    "author": "Team XYZ",
    "version": "0.3.1",
    "hooks": {
        "PageContentSave": "AutoProjectCategory\\Hooks::onPageContentSave"
    }
}
<?php
namespace AutoProjectCategory;

use MediaWiki\MediaWikiServices;
use Title;
use WikiPage;

class Hooks {
    public static function onPageContentSave( WikiPage $wikiPage, $user, $content,
        $summary, $isMinor, $isWatch ) {
        $title = $wikiPage->getTitle();
        if ( !$title->inNamespace( NS_PROJECT ) || $title->isRedirect() ) {
            return true;
        }

        // Simple cache to avoid re‑adding the category on every edit.
        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
        $key = $cache->makeKey( 'autocategory', $title->getArticleID() );
        if ( $cache->has( $key ) ) {
            return true;
        }

        // Append the category tag.
        $newText = $content->getNativeData() . "\n[[Category:Project]]        $content->setText( $newText );

        // Store cache entry for 5 minutes.
        $cache->add( $key, true, 300 );

        return true;
    }
}

Notice how the cache prevents redundant writes—nothing fancy, but it illustrates the “light‑weight” principle nicely.

Final Thoughts

Custom MediaWiki hooks are powerful, but with great power comes the responsibility to keep the wiki responsive, secure, and maintainable. By registering hooks cleanly in extension.json, writing concise, well‑typed callbacks, and backing everything with tests and documentation, you’ll end up with a sturdy piece of code that plays nicely with future MediaWiki releases.

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