Skip to content

Commit ae4fb81

Browse files
committed
[docs] Add more docs for new hooks
1 parent 2125821 commit ae4fb81

File tree

1 file changed

+180
-39
lines changed

1 file changed

+180
-39
lines changed

docs/apis/core/hooks/index.md

Lines changed: 180 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ tags:
88

99
import { Since } from '@site/src/components';
1010

11-
<Since version="4.3" />
11+
<Since version="4.3" issueNumber="MDL-74954" />
1212

1313
This page describes the Hooks API which is a replacement for some of the lib.php based one-to-many
1414
[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
1818
through hook callbacks in local plugins. For example adding a custom institution password
1919
policy that applies to all enabled authentication plugins through a new local plugin.
2020

21-
Hooks are not a means to implement or describe features of plugins of some type.
22-
2321
## General concepts
2422

2523
### Mapping to PSR-14
@@ -37,14 +35,14 @@ however the PSR-14 adherence has higher priority here.
3735

3836
### Hook emitter
3937

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
4139
to/from any other plugins. The exact type of information flow facilitated by hooks is not defined.
4240

4341
### Hook instance
4442

4543
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.
4846

4947
The names of hook classes should follow the standard pattern of general to more specific, this groups
5048
hooks for the same item when sorting alphabetically. For example `core\hook\course_delete_pre` instead
@@ -54,13 +52,13 @@ of `pre_course_delete`.
5452

5553
The code executing a hook does not know in advance which plugin is going to react to a hook.
5654

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
5957
whenever a relevant hook is dispatched.
6058

6159
### Hooks overview page
6260

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
6462
registered callbacks. It can be accessed by developers and administrators from the Site
6563
administration menu.
6664

@@ -72,25 +70,52 @@ callbacks completely:
7270

7371
```php title="/config.php"
7472
$CFG->hooks_callback_overrides = [
75-
'mod_activity\\hook\\installation_finished' => [
73+
\mod_activity\hook\installation_finished::class => [
7674
'test_otherplugin\\callbacks::activity_installation_finished' => ['disabled' => true],
77-
]
75+
],
7876
];
7977
```
8078

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
8283

8384
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
8790
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.
9193

9294
All hook classes should be defined as final, if needed traits can help with code reuse in similar hooks.
9395

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+
94119
### Example of hook creation
95120

96121
Imagine mod_activity plugin wants to notify other plugins that it finished installation,
@@ -101,7 +126,7 @@ installation process.
101126
<?php
102127
namespace mod_activity\hook;
103128

104-
class installation_finished implements \core\hook\describe_hook {
129+
class installation_finished implements \core\hook\described_hook {
105130
public static function get_hook_description(): string {
106131
return 'Hook dispatched at the very end of installation of mod_activity plugin.';
107132
}
@@ -120,16 +145,20 @@ function xmldb_activity_install() {
120145
## Registering of hook callbacks
121146

122147
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.
125153

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
128155

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
130157
methods must verify the plugin is in correct state. Often the easies way is to
131158
use function during_initial_install() or version string from the plugin configuration.
132159

160+
:::
161+
133162
### Example of hook callback registration
134163

135164
First developer needs to add a new static method to some class that accepts instance of
@@ -166,32 +195,38 @@ $callbacks = [
166195
];
167196
```
168197

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.
171200

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
174203
is installed.
175204

176205
## Deprecation of legacy lib.php callbacks
177206

178207
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.
180209

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
183212
then developers needs to only add extra parameter to existing legacy callback functions
184213
and the hook manager will trigger appropriated deprecated debugging messages when
185214
it detects plugins that were not converted to new hooks yet.
186215

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+
:::
190226

191227
## Example how to migrate legacy callback
192228

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`.
195230

196231
First we need a new hook:
197232

@@ -209,9 +244,8 @@ final class after_config implements described_hook, deprecated_callback_replacem
209244
}
210245
```
211246

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()`.
215249

216250
```php title="/lib/setup.php"
217251

@@ -226,6 +260,113 @@ foreach ($pluginswithfunction as $plugins) {
226260
}
227261
}
228262
}
263+
// Dispatch the new Hook implementation immediately after the legacy callback.
229264
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;
230279

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+
}
231372
```

0 commit comments

Comments
 (0)