-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[docs] Add \core\formatting and DI docs for MDL-80072
- Loading branch information
1 parent
eb7ffb3
commit 72e0ce7
Showing
3 changed files
with
349 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
--- | ||
title: Dependency Injection | ||
tags: | ||
- DI | ||
- Container | ||
- PSR-11 | ||
- PSR | ||
description: The use of PSR-11 compatible Dependency Injection in Moodle | ||
--- | ||
|
||
import { | ||
Since, | ||
ValidExample, | ||
InvalidExample, | ||
Tabs, | ||
TabItem, | ||
} from '@site/src/components'; | ||
|
||
<Since version="4.4" issueNumber="MDL-80072" /> | ||
|
||
Moodle supports the use of [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible Dependency Injection, accessed using the `\core\di` class, which internally makes use of [PHP-DI](https://php-di.org). All usage should be through the `\core\di` class. | ||
|
||
Most class instances can be fetched using their class name without any manual configuration. Support for configuration of constructor arguments is also possible, but is generally discouraged. | ||
|
||
Dependencies are stored using a string id attribute, which is typically the class or interface name of the dependency. Use of other arbitrary id values is strongly discouraged. | ||
|
||
## Fetching dependencies | ||
|
||
Moodle provides a wrapper around the PSR-11 Container implementation which should be used to fetch dependencies: | ||
|
||
```php title="Fetching an instance of the \core\http_client class" | ||
$client = \core\di::get(\core\http_client::class); | ||
``` | ||
|
||
## Configuring dependencies | ||
|
||
In some rare cases you may need to supply additional configuration for a dependency to work properly. This is usually in the case of legacy code, and can be achieved with the `\core\hook\di_configuration` hook. | ||
|
||
<Tabs> | ||
|
||
<TabItem value="config" label="Hook configuration"> | ||
|
||
The callback must be linked to the hook by specifying a callback in the plugin's `hooks.php` file: | ||
|
||
```php title="mod/example/db/hooks.php" | ||
<?php | ||
$callbacks = [ | ||
[ | ||
'hook' => \core\hook\di_configuration::class, | ||
'callback' => \mod_example\hook_listener::class . '::inject_dependenices', | ||
], | ||
]; | ||
``` | ||
|
||
</TabItem> | ||
|
||
<TabItem value="hook" label="Hook listener"> | ||
|
||
The hook listener consists of a static method on a class. | ||
|
||
```php title="mod/example/classes/hook_listener.php" | ||
<?php | ||
|
||
namespace mod_example; | ||
|
||
use core\hook\di_configuration; | ||
|
||
class hook_listener { | ||
public static function inject_dependencies(di_configuration $hook): void { | ||
$hook->add_definition( | ||
id: complex_client::class, | ||
definition: function ( | ||
\moodle_database $db, | ||
): complex_client { | ||
global $CFG; | ||
|
||
return new complex_client( | ||
db: $db, | ||
name: $CFG->some_value, | ||
); | ||
} | ||
) | ||
} | ||
} | ||
``` | ||
|
||
</TabItem> | ||
|
||
</Tabs> | ||
|
||
## Mocking dependencies in Unit Tests | ||
|
||
One of the most convenient features of Dependency Injection is the ability to provide a mocked version of the dependency during unit testing. | ||
|
||
Moodle resets the Dependency Injection Container between each unit test, which means that little-to-no cleanup is required. | ||
|
||
```php title="Injecting a Mocked dependency" | ||
<?php | ||
namespace mod_example; | ||
|
||
use GuzzleHttp\Handler\MockHandler; | ||
use GuzzleHttp\HandlerStack; | ||
use GuzzleHttp\Middleware; | ||
use GuzzleHttp\Psr7\Response; | ||
|
||
class example_test extends \advanced_testcase { | ||
public function test_the_thing(): void { | ||
// Mock our responses to the http_client. | ||
$handlerstack = HandlerStack::create(new MockHandler([ | ||
new Response(200, [], json_encode(['name' => 'Colin'])), | ||
])); | ||
|
||
// Inject the mock. | ||
\core\di::set( | ||
\core\http_client::class, | ||
new http_client(['handler' => $handlerstack]), | ||
); | ||
|
||
// Call a method on the example class. | ||
// This method uses \core\di to fetch the client and use it to fetch data. | ||
$example \core\di::get(example::class); | ||
$result = $example->do_the_thing(); | ||
|
||
// The result will be based on the mock response. | ||
$this->assertEquals('Colin', $result->get_name()); | ||
} | ||
} | ||
``` | ||
|
||
## Injecting the container into classes | ||
|
||
In more advanced cases you may wish to make use of dependency injection within an object. One way that you can do so is to inject the dependency into the class via its constructor, for example: | ||
|
||
```php title="Injecting the ContainerInterface" | ||
<?php | ||
|
||
namespace mod_example; | ||
|
||
use Psr\Container\ContainerInterface; | ||
|
||
class example { | ||
/** | ||
* @param ContainerInterface $container | ||
*/ | ||
public function __construct( | ||
protected ContainerInterface $container, | ||
) { | ||
} | ||
|
||
public function do_something(string $value): string { | ||
return $this->get(\core\formatting::class)->format_string($value); | ||
} | ||
} | ||
``` | ||
|
||
The `example` class can also be fetched using DI: | ||
|
||
```php title="Consuming the example class" | ||
use mod_example\example; | ||
|
||
// ... | ||
$example = \core\di::get(example::class); | ||
$value = $example->do_something("with this string"); | ||
``` | ||
|
||
## Advanced usage | ||
|
||
All usage of the Container _should_ be via `\core\di`, which is a wrapper around the currently-active Container implementation. In normal circumstances it is not necessary to access the underlying Container implementation directly and such usage is generally discouraged. | ||
|
||
:::danger | ||
|
||
Moodle currently makes use of PHP-DI as its Container implementation. | ||
|
||
Please note that the underlying Dependency Injection system _may_ change at any time. | ||
|
||
Care should be taken to only make use of methods which are defined in the [PSR-11 Container Interface](https://www.php-fig.org/psr/psr-11/). | ||
|
||
::: | ||
|
||
### Resetting the Container | ||
|
||
The Container is normally instantiated during the bootstrap phase of a script. In normal use it is not reset and there should be no need to reset it, however it is _possible_ to reset it if required. This usage is intended to be used for situations such as Unit Testing. | ||
|
||
:::danger | ||
|
||
Resetting an actively-used container can lead to unintended consequences. | ||
|
||
::: | ||
|
||
```php title="Resetting the Container" | ||
\core\di::reset_container(): | ||
``` | ||
|
||
### Accessing the Container Interface | ||
|
||
In some rare circumstances you may need to perform certain advanced tasks such as accessing the `ContainerInterface` implementation directly. | ||
|
||
The `\core\di` wrapper provides a method to fetch the `\Psr\Container\ContainerInterface` implementation. | ||
|
||
```php title="Fetch the ContainerInterface" | ||
$container = \core\di::get_container(); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.