Skip to content

Commit

Permalink
[docs] Add docs for attribute_helper and attribute-described hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewnicols committed Mar 7, 2024
1 parent 123da7a commit 5736452
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 21 deletions.
115 changes: 94 additions & 21 deletions docs/apis/core/hooks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ tags:
- core
---

import { Since, ValidExample } from '@site/src/components';
import { Since, ValidExample, Tabs, TabItem } from '@site/src/components';

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

Expand Down Expand Up @@ -62,7 +62,17 @@ to/from any other plugins. The exact type of information flow facilitated by hoo

Information passed between subsystem and plugins is encapsulated in arbitrary PHP class instances.
These can be in any namespace, but generally speaking they should be placed in the `some_component\hook\*`
namespace. Where possible, hooks are expected to implement the `core\hook\described_hook` interface.
namespace.

Hooks are encouraged to describe themselves and to provide relevant metadata to make them easier to use and discover. There are two ways to describe a hook:

- implement the `\core\hook\described_hook` interface, which has two methods:
- `get_description(): string;`
- `get_tags(): array;`
- add an instance of the following attributes to the class:
- `\core\attribute\label(string $description)`
- `\core\attribute\tags($a, $set, $of, $tags, ...)`
- `\core\attribute\hook\replaces_callbacks('a_list_of_legacy_callbacks', 'that_this_hook_replaces')`

### Hook callback

Expand Down Expand Up @@ -138,17 +148,52 @@ Imagine mod_activity plugin wants to notify other plugins that it finished insta
then mod_activity plugin developer adds a new hook and calls it at the end of plugin
installation process.

<Tabs>

<TabItem value="attribute-usage" label="Described by attribute" default>

```php title="/mod/activity/classes/hook/installation_finished.php"
<?php
namespace mod_activity\hook;

#[\core\attribute\label('Hook dispatched at the very end of installation of mod_activity plugin.')]
#[\core\attribute\tags('installation')]
class installation_finished implements \core\hook\described_hook {
public function __construct(
public function string $version,
) {
}
}
```

</TabItem>

<TabItem value="interface-usage" label="Described using Interface" default>

```php title="/mod/activity/classes/hook/installation_finished.php"
<?php
namespace mod_activity\hook;

class installation_finished implements \core\hook\described_hook {
public function __construct(
public function string $version,
) {
}

public static function get_hook_description(): string {
return 'Hook dispatched at the very end of installation of mod_activity plugin.';
}

public static function get_hook_tags(): array {
return ['installation'];
}
}
```

</TabItem>

</Tabs>

```php title="/mod/activity/db/install.php"
<?php
function xmldb_activity_install() {
Expand Down Expand Up @@ -246,6 +291,27 @@ This example describes migration of `after_config` callback from the very end of

First we need a new hook:

<Tabs>

<TabItem value="attribute-usage" label="Described by attribute" default>

```php title="/lib/classes/hook/after_config.php"
<?php
namespace core\hook;

use core\attribute;

#[attribute\label('Hook dispatched at the very end of lib/setup.php')]
#[attribute\tags('config')]
#[attribute\hook\replaces_callbacks('after_config')]
final class after_config {
}
```

</TabItem>

<TabItem value="interface-usage" label="Described using Interface" default>

```php title="/lib/classes/hook/after_config.php"
<?php
namespace core\hook;
Expand All @@ -254,12 +320,21 @@ final class after_config implements described_hook, deprecated_callback_replacem
public static function get_hook_description(): string {
return 'Hook dispatched at the very end of lib/setup.php';
}

public static function get_hook_tags(): array {
return ['config'];
}

public static function get_deprecated_plugin_callbacks(): array {
return ['after_config'];
}
}
```

</TabItem>

</Tabs>

The hook needs to be emitted immediately after the current callback execution code,
and an extra parameter `$migratedtohook` must be set to true in the call to `get_plugins_with_function()`.

Expand Down Expand Up @@ -293,19 +368,12 @@ Since the hook is an arbitrary PHP object, it is possible to create any range of

namespace core\hook;

final class block_delete_pre implements described_hook, deprecated_callback_replacement {
public static function get_hook_description(): string {
return 'A hook dispatched just before a block instance is deleted';
}

#[\core\attribute\label('A hook dispatched just before a block instance is deleted')]
final class block_delete_pre implements deprecated_callback_replacement {
public function __construct(
protected stdClass $blockinstance,
public readonly \stdClass $blockinstance,
) {}

public function get_instance(): stdClass {
return $this->blockinstance;
}

public static function get_deprecated_plugin_callbacks(): array {
return ['pre_block_delete'];
}
Expand Down Expand Up @@ -341,23 +409,19 @@ To make use of Stoppable events, the hook simply needs to implement the `Psr\Eve

namespace core\hook;

use core\attribute;

#[attribute\label('A hook dispatched just before a block instance is deleted')]
#[attribute\hook\replaces_callbacks('pre_block_delete')]
final class block_delete_pre implements
described_hook,
deprecated_callback_replacement.
Psr\EventDispatcher\StoppableEventInterface
{
public static function get_hook_description(): string {
return 'A hook dispatched just before a block instance is deleted';
}

public function __construct(
protected stdClass $blockinstance,
public readonly \stdClass $blockinstance,
) {}

public function get_instance(): stdClass {
return $this->blockinstance;
}

public function isPropagationStopped(): bool {
return $this->stopped;
}
Expand Down Expand Up @@ -386,3 +450,12 @@ class callbacks {
}
}
```

## Tips and Tricks

Whilst not being formal requirements, you are encouraged to:

- describe and tag your hook as appropriate using either:
- the `\core\hook\described_hook` interface; or
- the `\core\attribute\label` and `\core\attribute\tags` attributes
- make use of constructor property promotion combined with readonly properties to reduce unnecessary boilerplate.
74 changes: 74 additions & 0 deletions docs/devupdate.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,80 @@ This page highlights the important changes that are coming in Moodle 4.4 for dev

Support for PSR-11 compatible Containers has been introduced and can be accessed via the `\core\di` class. Read the [full documentation](./apis/core/di/index.md) for information on how to use Moodle's DI infrastructure.

### Attributes

<Since version="4.4" issueNumber="MDL-81011" />

PHP 8.0 introduced support for the Attribute language feature and Moodle is beginning to make use of this in small but helpful ways.

To simplify this adoption, a new `\core\attribute_helper` class has been created with methods to quickly and easily fetch the `\ReflectionAttribute` for any relevant attributes.

The new methods can search for an attribute using a reference, which can be:

- a string, whose name represents a global function, a class, or a class combined with a method, property, constant, or enum
- an instantiated object
- an array whose first value is either a string or object, and whose optional second value is a child of the first value

```php title="Fetching attributes"
<?php

use core\attribute_helper;
use core\attribute\{label, tags};

// Get a label on the \some_function_name() method.
$label = attribute_helper::one_from('some_function_name', label::class)?->newInstance();

// Get an instance of a label on the \example class.
$label = attribute_helper::one_from(example::class, label::class)?->newInstance();

// Get an instance of a label on an instance of the \example class.
$example = new example();
$label = attribute_helper::one_from($example, label::class)?->newInstance();

// Get an instance of a label on a constant, property, or method of the \example class.
$label = attribute_helper::one_from([example::class, 'some_child'], label::class)?->newInstance();

// Get an instance of a label on a constant, property, or method of an instance of the \example class.
$example = new example();
$label = attribute_helper::one_from([$example, 'some_child'], label::class)?->newInstance();
```

Other variations of the above are also possible.

### Hooks

<Since version="4.4" issueNumber="MDL-81011" />

Attribute-based alternatives to the `\core\hook\described_hook`, and `\core\hook\deprecated_callback_replacement` interfaces are now supported.

- the `\core\attribute\label` attribute can be used to add a string-based description of the hook.
- the `\core\attribute\tags` attribute can be used to add one or more tags to describe the hook.
- the `\core\attribute\hook\replaces_callbacks` attribute can be used to add one or more replaced callbacks.

```php
<?php

namespace core\hook;

use core\attribute;

#[attribute\label('This is a description of the hook')]
#[attribute\tags('examples', 'navigation', 'authentication')]
#[attribute\hook\replaces_callbacks('an_old_callback_that_is_replaced_here')]
class before_navigation_render {
public function __construct(
public readonly \navigation_node $navigation,
) {
}
}
```

:::tip

It is still possible to use the `\core\hook\described_hook` and `\core\hook\deprecated_callback_replacement` interfaces. The attribute approach is provided as a more light-weight alternative.

:::

### String formatting

#### Deprecation of format_* parameters
Expand Down

0 comments on commit 5736452

Please sign in to comment.