1Hooks 2===== 3 4## Introduction 5 6Hooks allow MediaWiki Core to call extensions or allow one extension to call 7another extension. For more information and a list of hooks, see 8https://www.mediawiki.org/wiki/Manual:Hooks 9 10Starting in MediaWiki 1.35, each hook called by MediaWiki Core has an 11associated interface with a single method. To call the hook, obtain a "hook 12runner" object, which implements the relevant interface, and call the relevant 13method. To handle a hook event in an extension, create a handler object which 14implements the interface. 15 16The name of the interface is the name of the hook with "Hook" added to the end. 17Interfaces are typically placed in the namespace of their primary caller. 18 19The method name for the hook is the name of the hook, prefixed with "on". 20 21Several hooks had colons in their name, which are invalid in an interface or 22method name. These hooks have interfaces and method names in which the colons 23are replaced with underscores. 24 25For example, if the hook is called `Mash`, we might have the interface: 26 27 interface MashHook { 28 public function onMash( $banana ); 29 } 30 31Hooks can be defined and called by extensions. The extension should define a 32hook interface for each hook, as described above. 33 34## HookContainer 35 36HookContainer is a service which is responsible for maintaining a list of hook 37handlers and calling those handlers when requested. HookContainer is not aware 38of hook interfaces or parameter types. 39 40HookContainer provides hook metadata. For example, `isRegistered()` tells us 41whether there are any handlers for a given hook event. 42 43A HookContainer instance can be obtained from the global service locator with 44MediaWikiServices::getHookContainer(). When implementing a service that needs 45to call a hook, a HookContainer object should be passed to the constructor of 46the service. 47 48## Hook runner classes 49 50A hook runner is a class which implements hook interfaces, proxying the calls 51to `HookContainer::run()`. 52 53MediaWiki has two hook runner classes: HookRunner and ApiHookRunner. 54ApiHookRunner has proxy methods for all hooks which are called by the Action 55API. HookRunner has proxy methods for all hooks which are called by other parts 56of Core. Some hooks are implemented in both classes. 57 58Extensions which call hooks should create their own hook runner class, by 59analogy with the ones in Core. Hook runner classes are effectively internal 60to the module which calls the relevant hooks. Reorganisation of the hook 61calling code may lead to methods being removed from hook runner classes. Thus, 62it is safer for extensions to define their own hook runner classes even if 63they are calling Core hooks. 64 65New code should typically be written in a service which takes a HookContainer 66as a constructor parameter. However, for the convenience of existing static 67functions in MediaWiki Core, `Hooks::runner()` may be used to obtain a 68HookRunner instance. This is equivalent to 69 70 new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) 71 72For example, to call the hook `Mash`, as defined above, in static code: 73 74 Hooks::runner()->onMash( $banana ); 75 76## How to handle a hook event in an extension 77 78In extension.json, there is a new attribute called `HookHandlers`. This is 79an object mapping the handler name to an ObjectFactory specification describing 80how to create the handler object. The specification will typically have a 81`class` member with the name of the handler class. For example, in an extension 82called `FoodProcessor`, we may have: 83 84 "HookHandlers": { 85 "main": { 86 "class": "MediaWiki\\Extension\\FoodProcessor\\HookHandler" 87 } 88 } 89 90Then in the Hooks attribute, instead of a function name, the value will be the 91handler name: 92 93 "Hooks": { 94 "Mash": "main" 95 } 96 97Or more explicitly, by using an object instead of a string for the handler: 98 99 "Hooks": { 100 "Mash": { 101 "handler": "main" 102 } 103 } 104 105Note that while your HookHandler class will implement an interface that ends 106with the word "Hook", in `extension.json` you should omit the word "Hook" 107from the key in the `Hooks` definition. For example, in the definitions above, 108the key must be "Mash", not "MashHook". 109 110Then the extension will define a handler class: 111 112 namespace MediaWiki\Extension\FoodProcessor; 113 114 class HookHandler implements MashHook { 115 public function onMash( $banana ) { 116 // Implementation goes here 117 } 118 } 119 120## Service dependencies 121 122The ObjectFactory specification in HookHandlers can contain a list of services 123which should be instantiated and provided to the constructor or factory 124function for the handler. For example: 125 126 "HookHandlers": { 127 "main": { 128 "class": "MediaWiki\\Extension\\FoodProcessor\\HookHandler", 129 "services": [ "ReadOnlyMode" ] 130 } 131 } 132 133However, care should be taken with this feature. Some services have expensive 134constructors, so requesting them when handling commonly-called hooks may damage 135performance. Also, some services may not be safe to construct from within a hook 136call. 137 138The safest pattern for service injection is to use a separate handler for each 139hook, and to inject only the services needed by that hook. 140 141Calling a hook with the `noServices` option disables service injection. If a 142handler for such a hook specifies services, an exception will be thrown when 143the hook is called. 144 145## Returning and aborting 146 147If a hook handler returns false, HookContainer will stop iterating through the 148list of handlers and will immediately return false. 149 150If a hook handler returns true, or if there is no return value (causing it to 151effectively return null), then HookContainer will continue to call any other 152remaining handlers. Eventually HookContainer::run() will return true. 153 154If there were no registered handlers, HookContainer::run() will return true. 155 156Some hooks are declared to be "not abortable". If a handler for a non-abortable 157hook returns false, an exception will be thrown. A hook is declared to be not 158abortable by passing `[ "abortable" => false ]` in the $options parameter to 159HookContainer::run(). 160 161Aborting is properly used to enforce a convention that only one extension 162may handle a given hook call. 163 164Aborting is sometimes used as a generic return value, to indicate that the 165caller should stop performing some action. 166 167Most hook callers do not check the return value from HookContainer::run() and 168there is no real concept of aborting. The only effect of returning `false` from 169a handler of these hooks is to break other extensions. 170 171Theoretically, extensions which are registered first in LocalSettings.php will 172be called first, and thus will have the first opportunity to abort a hook call. 173This behaviour should not be relied upon. In the new hook system, handlers 174registered in the legacy way are called first, before handlers registered in 175the new way. 176 177## Parameters passed by reference 178 179The typical way for a handler to return data to the caller is by modifying a 180parameter which was passed by reference. This is sometimes called "replacement". 181 182Reference parameters were somewhat overused in early versions of MediaWiki. You 183may find that some parameters passed by reference cannot reasonably be modified. 184Replacement either has no effect on the caller or would cause unexpected or 185inconsistent effects. Handlers should generally only replace a parameter when it 186is clear from the documentation that replacement is expected. 187 188## How to define a new hook 189 190* Create a hook interface, typically in a subnamespace called `Hook` relative 191 to the caller namespace. For example, if the caller is in a namespace called 192 `MediaWiki\Foo`, the hook interface might be placed in `MediaWiki\Foo\Hook`. 193* Add an implementation to the relevant HookRunner class. 194 195## Hook deprecation 196 197Core hooks are deprecated by adding them to an array in the DeprecatedHooks 198class. Hooks declared in extensions may be deprecated by listing them in the 199`DeprecatedHooks` attribute: 200 201 "DeprecatedHooks": { 202 "Mash": { 203 "deprecatedVersion": "2.0", 204 "component": "FoodProcessor" 205 } 206 } 207 208If the `component` is not specified, it defaults to the name of the extension. 209 210The hook interface should be marked as deprecated by adding @deprecated to the 211interface doc comment. The interface doc comment is a better place for 212@deprecated than the method doc comment, because this causes the interface to 213be deprecated for implementation. Deprecating the method only causes calling 214to be deprecated, not handling. 215 216Deprecating a hook in this way activates a migration system called 217**call filtering**. Extensions opt in to call filtering of deprecated hooks by 218**acknowledging** deprecation. An extension acknowledges deprecation with the 219`deprecated` parameter in the `Hooks` attribute: 220 221 "Hooks": { 222 "Mash": { 223 "handler": "main", 224 "deprecated": "true" 225 } 226 } 227 228If deprecation is acknowledged by the extension: 229 230* If MediaWiki knows that the hook is deprecated, the handler will not be 231 called. The call to the handler is filtered. 232* If MediaWiki does not have the hook in its list of deprecated hooks, the 233 handler will be called anyway. 234 235Deprecation acknowledgement is a way for the extension to say that it has made 236some other arrangement for implementing the relevant functionality and does 237not need the handler for the deprecated hook to be called. 238 239### Call filtering example 240 241Suppose the hook `Mash` is deprecated in MediaWiki 2.0, and is replaced by a 242new one called `Slice`. In our example extension FoodProcessor 1.0, the 243`Mash` hook is handled. In FoodProcessor 2.0, both `Mash` and `Slice` have 244handlers, but deprecation of `Mash` is acknowledged. Thus: 245 246* With MediaWiki 2.0 and FoodProcessor 1.0, `onMash` is called but raises a 247 deprecation warning. 248* With MediaWiki 2.0 and FoodProcessor 2.0, `onMash` is filtered, and `onSlice` 249 is called. 250* With MediaWiki 1.0 and FoodProcessor 2.0, `onMash` is called, since it is not 251 yet deprecated in Core. `onSlice` is not called since it does not yet exist 252 in Core. 253 254So the call filtering system provides both forwards and backwards compatibility. 255 256### Silent deprecation 257 258Developers sometimes use two stages of deprecation: "soft" deprecation in which 259the deprecated entity is merely discouraged in documentation, and "hard" 260deprecation in which a warning is raised. When you soft-deprecate a hook, it is 261important to register it as deprecated so that call filtering is activated. 262Activating call filtering simplifies the task of migrating extensions to the 263new hook. 264 265To deprecate a hook without raising deprecation warnings, use the "silent" flag: 266 267 "DeprecatedHooks": { 268 "Mash": { 269 "deprecatedVersion": "2.0", 270 "component": "FoodProcessor", 271 "silent": true 272 } 273 } 274 275As with hard deprecation, @deprecated should be added to the interface. 276