8
8
9
9
import { Since } from '@site/src /components';
10
10
11
- <Since version =" 4.3 " />
11
+ <Since version =" 4.3 " issueNumber = " MDL-74954 " />
12
12
13
13
This page describes the Hooks API which is a replacement for some of the lib.php based one-to-many
14
14
[ plugin callbacks] ( https://docs.moodle.org/dev/Callbacks ) implementing on
@@ -18,8 +18,6 @@ The most common use case for hooks is to allow customisation of standard plugins
18
18
through hook callbacks in local plugins. For example adding a custom institution password
19
19
policy that applies to all enabled authentication plugins through a new local plugin.
20
20
21
- Hooks are not a means to implement or describe features of plugins of some type.
22
-
23
21
## General concepts
24
22
25
23
### Mapping to PSR-14
@@ -37,14 +35,14 @@ however the PSR-14 adherence has higher priority here.
37
35
38
36
### Hook emitter
39
37
40
- Hook emitter is a place in code where core or a plugin needs to send or receive information
38
+ A _ Hook emitter _ is a place in code where core or a plugin needs to send or receive information
41
39
to/from any other plugins. The exact type of information flow facilitated by hooks is not defined.
42
40
43
41
### Hook instance
44
42
45
43
Information passed between subsystem and plugins is encapsulated in arbitrary PHP class instances.
46
- For now Moodle hooks are expected to be placed in ` some_component\hook\* ` namespaces and they
47
- are expected to implement ` core\hook\described_hook ` interface.
44
+ These can be in any namespace, but generally speaking they should be placed in the ` some_component\hook\* `
45
+ namespace. Where possible, hooks are expected to implement the ` core\hook\described_hook ` interface.
48
46
49
47
The names of hook classes should follow the standard pattern of general to more specific, this groups
50
48
hooks for the same item when sorting alphabetically. For example ` core\hook\course_delete_pre ` instead
@@ -54,13 +52,13 @@ of `pre_course_delete`.
54
52
55
53
The code executing a hook does not know in advance which plugin is going to react to a hook.
56
54
57
- System maintains ordered list of callbacks for each class of hook. Any plugin is free to register
58
- hook callbacks by adding a db/hooks.php file. The specified plugin callback method is called
55
+ Moodle maintains an ordered list of callbacks for each class of hook. Any plugin is free to register
56
+ its own hook callbacks by creating a ` db/hooks.php ` file. The specified plugin callback method is called
59
57
whenever a relevant hook is dispatched.
60
58
61
59
### Hooks overview page
62
60
63
- ** Hooks overview page** lists all hooks that may be triggered in the system together with all
61
+ The ** Hooks overview page** lists all hooks that may be triggered in the system together with all
64
62
registered callbacks. It can be accessed by developers and administrators from the Site
65
63
administration menu.
66
64
@@ -72,25 +70,52 @@ callbacks completely:
72
70
73
71
``` php title="/config.php"
74
72
$CFG->hooks_callback_overrides = [
75
- ' mod_activity\\ hook\\ installation_finished' => [
73
+ \ mod_activity\hook\installation_finished::class => [
76
74
'test_otherplugin\\callbacks::activity_installation_finished' => ['disabled' => true],
77
- ]
75
+ ],
78
76
];
79
77
```
80
78
81
- ## Adding of new hooks
79
+ The hooks overview page will automatically list any hook which is placed inside any ` *\hook\* ` namespace within any Moodle component.
80
+ If you define a hook which is _ not_ in this namespace then you ** must** also define a new ` \core\hook\discovery_agent ` implementation in ` [component]\hooks ` .
81
+
82
+ ## Adding new hooks
82
83
83
84
1 . Developer first identifies a place where they need to ask or inform other plugins about something.
84
- 2 . Depending on the location a new class implementing core\hook\described_hook is added to core\hook\* or
85
- some_plugin\hook\* namespace.
86
- 3 . Optionally if any data needs to be sent to hook callbacks developer needs to add internal hook
85
+ 1 . Depending on the location a new class implementing ` core\hook\described_hook ` is added to ` core\hook\* ` or
86
+ ` some_plugin\hook\* ` namespace as appropriate.
87
+ 1 . Optionally the developer may wish to allow the callback to stop any subsequent callbacks from receiving the object.
88
+ If so, then the object should implement the ` Psr\EventDispatcher\StoppableEventInterface ` interface.
89
+ 1 . Optionally if any data needs to be sent to hook callbacks, the developer may add internal hook
87
90
constructor, some instance properties for data storage and public methods for data access from callbacks.
88
- 4 . Optionally hook class may also implement public methods to add information that is passed back
89
- to the original hook execution point, or simply depend on objects passed by reference as hook data.
90
- 5 . Hooks may have stoppable interface which may be used to stop execution of remaining callbacks.
91
+
92
+ Hook classes may be any class you like. When designing a new Hook, you should think about how consumers may wish to change the data they are passed.
91
93
92
94
All hook classes should be defined as final, if needed traits can help with code reuse in similar hooks.
93
95
96
+ :::important Hooks not located in standard locations
97
+
98
+ If you define a hook which is _ not_ in the ` [component]\hook\* ` namespace then you ** must** also define a new ` \core\hook\discovery_agent ` implementation in ` [component]\hooks ` .
99
+
100
+ ``` php title="/mod/example/classes/hooks.php"
101
+ <?php
102
+
103
+ namespace mod_example;
104
+
105
+ class hooks implements \core\hook\hook_discovery_agent {
106
+ public static function discover_hooks(): array {
107
+ return [
108
+ [
109
+ 'class' => \mod_example\local\entitychanges\create_example::class,
110
+ 'description' => 'A hook fired when an example was created',
111
+ ],
112
+ ];
113
+ }
114
+ }
115
+ ```
116
+
117
+ :::
118
+
94
119
### Example of hook creation
95
120
96
121
Imagine mod_activity plugin wants to notify other plugins that it finished installation,
@@ -101,7 +126,7 @@ installation process.
101
126
<?php
102
127
namespace mod_activity\hook;
103
128
104
- class installation_finished implements \core\hook\describe_hook {
129
+ class installation_finished implements \core\hook\described_hook {
105
130
public static function get_hook_description(): string {
106
131
return 'Hook dispatched at the very end of installation of mod_activity plugin.';
107
132
}
@@ -120,16 +145,20 @@ function xmldb_activity_install() {
120
145
## Registering of hook callbacks
121
146
122
147
Any plugin is free to register callbacks for all core and plugin hooks.
123
- The registration is done by adding a db/hooks.php file to plugin.
124
- Callbacks must be provided as PHP callable strings in the form of "some\class\name::static_method".
148
+ The registration is done by adding a ` db/hooks.php ` file to plugin.
149
+ Callbacks ** must** be provided as PHP callable strings in the form of "some\class\name::static_method".
150
+
151
+ Hook callbacks are executed in the order of their priority from highest to lowest.
152
+ Any guidelines for callback priority should be described in hook descriptions if necessary.
125
153
126
- Hook callbacks are executed in the order of their priority, the rules
127
- for priority numbers should be described in hook descriptions if necessary.
154
+ ::: important
128
155
129
- Callbacks are executed also during system installation and all upgrades , the callback
156
+ Callbacks _ are executed during system installation and all upgrades _ , the callback
130
157
methods must verify the plugin is in correct state. Often the easies way is to
131
158
use function during_initial_install() or version string from the plugin configuration.
132
159
160
+ :::
161
+
133
162
### Example of hook callback registration
134
163
135
164
First developer needs to add a new static method to some class that accepts instance of
@@ -166,32 +195,38 @@ $callbacks = [
166
195
];
167
196
```
168
197
169
- Callback registrations are cached, so developer needs to either bump the local_stuff version
170
- or administrators need to purge all caches.
198
+ Callback registrations are cached, so developers should to either increment the version number for the
199
+ component they place the hook into. During development it is also possible to purge caches.
171
200
172
- In this particular example developer would probably also add some code to db/install.php
173
- to perform the necessary action in case the hook gets called before the local_stuff plugin
201
+ In this particular example, the developer would probably also add some code to ` db/install.php `
202
+ to perform the necessary action in case the hook gets called before the ` local_stuff ` plugin
174
203
is installed.
175
204
176
205
## Deprecation of legacy lib.php callbacks
177
206
178
207
Hooks are a direct replacement for one-to-many lib.php callback functions that were implemented
179
- using get_plugins_with_function(), plugin_callback() or component_callback() functions.
208
+ using the ` get_plugins_with_function() ` , ` plugin_callback() ` , or ` component_callback() ` functions.
180
209
181
- If hook implements ` core\hook\deprecated_callback_replacement ` and if deprecated lib.php
182
- callbacks can be listed in get_deprecated_plugin_callbacks() hook method
210
+ If a hook implements the ` core\hook\deprecated_callback_replacement ` interface, and if deprecated ` lib.php `
211
+ callbacks can be listed in ` get_deprecated_plugin_callbacks() ` hook method
183
212
then developers needs to only add extra parameter to existing legacy callback functions
184
213
and the hook manager will trigger appropriated deprecated debugging messages when
185
214
it detects plugins that were not converted to new hooks yet.
186
215
187
- Please note it is possible for plugin to contain both legacy lib.php callback and hook
188
- callback so that 3rd party plugins can be made compatible with multiple Moodle branches.
189
- The legacy lib.php callbacks are automatically ignored if hook callback is present.
216
+ :::important Legacy fallback
217
+
218
+ Please note ** it is** possible for plugin to contain both legacy ` lib.php ` callback and PSR-14 hook
219
+ callbacks.
220
+
221
+ This allows community contributed plugins to be made compatible with multiple Moodle branches.
222
+
223
+ The legacy ` lib.php ` callbacks are automatically ignored if hook callback is present.
224
+
225
+ :::
190
226
191
227
## Example how to migrate legacy callback
192
228
193
- This example describes migration of after_config callback from the very end of lib/setup.php
194
- file.
229
+ This example describes migration of ` after_config ` callback from the very end of ` lib/setup.php ` .
195
230
196
231
First we need a new hook:
197
232
@@ -209,9 +244,8 @@ final class after_config implements described_hook, deprecated_callback_replacem
209
244
}
210
245
```
211
246
212
- Then hook needs to be added right after the current place of callback execution
213
- and an extra parameter $migratedtohook has to be set to true in get_plugins_with_function()
214
- call.
247
+ The hook needs to be emitted immediately after the current callback execution code,
248
+ and an extra parameter ` $migratedtohook ` must be set to true in the call to ` get_plugins_with_function() ` .
215
249
216
250
``` php title="/lib/setup.php"
217
251
@@ -226,6 +260,113 @@ foreach ($pluginswithfunction as $plugins) {
226
260
}
227
261
}
228
262
}
263
+ // Dispatch the new Hook implementation immediately after the legacy callback.
229
264
core\hook\manager::get_instance()->dispatch(new core\hook\after_config());
265
+ ```
266
+
267
+ ## Hooks which contain data
268
+
269
+ It is often desirable to pass a data object when dispatching hooks.
270
+
271
+ This can be useful where you are passing code that consumers may wish to change.
272
+
273
+ Since the hook is an arbitrary PHP object, it is possible to create any range of public data and/or method you like and for the callbacks to use those methods and properties for later consumption.
274
+
275
+ ``` php title="/lib/classes/hook/block_delete_pre.php"
276
+ <?php
277
+
278
+ namespace core\hook;
230
279
280
+ final class block_delete_pre implements described_hook, deprecated_callback_replacement {
281
+ public static function get_hook_description(): string {
282
+ return 'A hook dispatched just before a block instance is deleted';
283
+ }
284
+
285
+ public function __construct(
286
+ protected stdClass $blockinstance,
287
+ ) {}
288
+
289
+ public function get_instance(): stdClass {
290
+ return $this->blockinstance;
291
+ }
292
+
293
+ public static function get_deprecated_plugin_callbacks(): array {
294
+ return ['pre_block_delete'];
295
+ }
296
+ }
297
+ ```
298
+
299
+ When dispatching the hook, it behaves as any other normal PHP Object:
300
+
301
+ ``` php title="/lib/blocklib.php"
302
+ // Allow plugins to use this block before we completely delete it.
303
+ if ($pluginsfunction = get_plugins_with_function('pre_block_delete', 'lib.php', true, true)) {
304
+ foreach ($pluginsfunction as $plugintype => $plugins) {
305
+ foreach ($plugins as $pluginfunction) {
306
+ $pluginfunction($instance);
307
+ }
308
+ }
309
+ }
310
+ }
311
+ $hook = new \core\hook\block_delete_pre($instance);
312
+ core\hook\manager::get_instance()->dispatch($hook);
313
+ ```
314
+
315
+ ## Hooks which can be stopped
316
+
317
+ In some situations it is desirable to allow a callback to stop execution of a hook. This can happen in situations where the hook contains that should only be set once.
318
+
319
+ The Moodle hooks implementation has support for the full PSR-14 specification, including Stoppable Events.
320
+
321
+ To make use of Stoppable events, the hook simply needs to implement the ` Psr\EventDispatcher\StoppableEventInterface ` interface.
322
+
323
+ ``` php title="/lib/classes/hook/block_delete_pre.php"
324
+ <?php
325
+
326
+ namespace core\hook;
327
+
328
+ final class block_delete_pre implements
329
+ described_hook,
330
+ deprecated_callback_replacement.
331
+ Psr\EventDispatcher\StoppableEventInterface
332
+ {
333
+ public static function get_hook_description(): string {
334
+ return 'A hook dispatched just before a block instance is deleted';
335
+ }
336
+
337
+ public function __construct(
338
+ protected stdClass $blockinstance,
339
+ ) {}
340
+
341
+ public function get_instance(): stdClass {
342
+ return $this->blockinstance;
343
+ }
344
+
345
+ public function isPropagationStopped(): bool {
346
+ return $this->stopped;
347
+ }
348
+
349
+ public function stop(): void {
350
+ $this->stopped = true;
351
+ }
352
+
353
+ public static function get_deprecated_plugin_callbacks(): array {
354
+ return ['pre_block_delete'];
355
+ }
356
+ }
357
+ ```
358
+
359
+ A callback will only be called if the hook was not stopped before-hand. Depending on the hook implementation, it can stop he
360
+
361
+ ``` php title="/local/myplugin/classes/callbacks.php"
362
+ <?php
363
+
364
+ namespace local_myplugin;
365
+
366
+ class callbacks {
367
+ public static function block_pre_delete(\core\hook\block_delete_pre $hook): void {
368
+ // ...
369
+ $hook->stop();
370
+ }
371
+ }
231
372
```
0 commit comments