Skip to main content
Version: main (5.3)

Dependency Injection

Since 4.4

Moodle supports the use of PSR-11 compatible Dependency Injection, accessed using the \core\di class, which internally makes use of PHP-DI.

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

When accessing dependencies within a class, it is advisable to inject them into the constructor, for example:

Fetching a instance of the \core\http_client class from within a class
class my_thing {
public function __construct(
protected readonly \core\http_client $client,
) {
}
}

For legacy code, or for scripts accessing an injected class, Moodle provides a wrapper around the PSR-11 Container implementation which can be used to fetch dependencies:

Fetching dependencies using the DI container
// Fetching an instance of the \core\http_client class outside of a class.
$client = \core\di::get(\core\http_client::class);

// Fetching an instance of a class which is managed using DI.
$thing = \core\di::get(my_thing::class);

\core\di::get() always returns the same shared instance for a given entry. When a fresh instance is required each time, for example when replacing new $classname() in legacy factory code, use \core\di::make() instead.

Building a new instance each time

Since 5.3

\core\di::make() behaves like \core\di::get(), except that it resolves the entry again on every call. If the entry is a class, a new instance is built each time, making the container behave like a factory:

Building a new instance using \core\di::make()
// Each call returns a brand new instance of my_thing.
$thing1 = \core\di::make(my_thing::class);
$thing2 = \core\di::make(my_thing::class);

Optional parameters can also be passed to force specific constructor arguments to specific values. Any constructor parameters not provided are resolved using the container as normal:

Building a new instance with specific parameters
$renderer = \core\di::make(\core\output\core_renderer::class, [
'page' => new \moodle_page(),
'target' => \RENDERER_TARGET_CLI,
]);

\core\di::make() is particularly useful for legacy entry points which directly instantiate a class (for example new $classname()), and for factories, where a new object is expected on each call.

Constructor Property Promotion and Readonly properties

When using constructor-based injection, you can simplify your dependency injection by making use of Constructor Property Promotion, and Readonly properties.

The use of readonly properties is also highly recommended as it ensures that dependencies cannot be inadvertently changed.

These language features are available in all Moodle versions supporting Dependency Injection.

class example_without_promotion {
protected \core\http_client $client;

public function __construct(
\core\http_client $client,
) {
$this->client = $client;
}
}

class example_with_promotion {
public function __construct(
protected readonly \core\http_client $client,
) {
}
}

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.

The callback must be linked to the hook by specifying a callback in the plugin's hooks.php file:

mod/example/db/hooks.php
<?php
$callbacks = [
[
'hook' => \core\hook\di_configuration::class,
'callback' => \mod_example\hook_listener::class . '::inject_dependencies',
],
];

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.

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 dependencies

Dependencies can be usually be easily injected into classes which are themselves loaded using Dependency Injection.

In most cases in Moodle, this should be via the class constructor, for example:

Injecting via the constructor
class thing_manager {
public function __construct(
protected readonly \moodle_database $db,
) {
}

public function get_things(): array {
return $this->db->get_records('example_things');
}
}

// Fetching the injected class from legacy code:
$manager = \core\di::get(thing_manager::class);
$things = $manager->get_things();

// Using it in a child class:
class other_thing {
public function __construct(
protected readonly thing_manager $manager,
) {
}

public function manage_things(): void {
$this->manager->get_things();
}
}
A note on injecting the Container

It is generally inadvisable to inject the Container itself. Please do not inject the \Psr\Container\ContainerInterface.

Attribute-based injection

Since 5.3

As an alternative to constructor injection, dependencies can be injected directly onto a property using the PHP-DI #[\DI\Attribute\Inject] attribute:

Injecting a dependency onto a property using an attribute
class example_class {
#[\DI\Attribute\Inject]
private \core\formatting $formatter;
}

The property is populated automatically whenever the class is built through the container, whether the instance is fetched using \core\di::get(), or a new instance is built using \core\di::make():

Fetching a class which uses attribute-based injection
// Fetch the example class using the `get` method for entries stored in the container.
$example1 = \core\di::get(example_class::class);

// Fetch the example class using the `make` method for entries built on each call.
$example2 = \core\di::make(example_class::class);

Attribute-based injection is the recommended approach for use in controllers, and makes it easier to support the use of Dependency Injection in other areas of the codebase, such as legacy code and factories using the make method, without needing to change how the class is constructed.

Constructor injection is still preferred

Where possible, prefer constructor injection over attribute-based injection as it makes a class's dependencies explicit and keeps the class usable without the container, for example directly within unit tests. Attribute-based injection is most useful where a class is already constructed by other code and its constructor signature cannot easily be changed.

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.

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.

Unit testing

The container is already reset after each test when running unit tests. It is not necessary nor recommended to so manually.

Resetting the Container
\core\di::reset_container();
danger

Resetting an actively-used container can lead to unintended consequences.