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.jsonwith 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:
- 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.
- Avoid heavy work. Hooks run in the request cycle; a slow database query here will make the whole save feel sluggish.
- Never throw uncaught exceptions. If something goes sideways, log it and return
trueso 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
SELECTrather 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:
- Prefer
MockObjects over real file writes; otherwise tests become flaky. - Run the suite with
phpunit --group MyAwesomeHookto 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
WikiPageAPI; 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:
- When does the hook fire? (e.g., after a page is saved, before an edit is parsed)
- What parameters are passed? (list types and a brief description)
- What should the callback return? (usually
trueto continue,falseto 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 asnull, which MediaWiki treats likefalseand aborts the operation. - Modifying globals. Changing
$wgUseror other globals inside a hook can create hard‑to‑track side effects. - Duplicate registrations. Register the same hook in both
extension.jsonandLocalSettings.php; you’ll get double execution. - Assuming $wikiPage is always new. In
PageContentSaveyou 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.