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