diff --git a/docs/apis/core/clock/index.md b/docs/apis/core/clock/index.md
index de70bd68b..c7ae31584 100644
--- a/docs/apis/core/clock/index.md
+++ b/docs/apis/core/clock/index.md
@@ -11,7 +11,8 @@ description: Fetching the current time
-Moodle supports use of a [PSR-20](https://php-fig.org/psr/psr-20/) compatible Clock interface, which should be accessed using Dependency Injection.
+Moodle provides a [PSR-20](https://php-fig.org/psr/psr-20/) compatible Clock interface. Classes which need the current time
+should receive `\core\clock` through constructor injection.
This should be used instead of `time()` to fetch the current time. This allows unit tests to mock time and therefore to test a variety of cases such as events happening at the same time, or setting an explicit time.
@@ -23,34 +24,22 @@ We recommend that the Clock Interface is used consistently in your code instead
## Usage {/* #usage */}
-The usage of the Clock extends the PSR-20 Clock Interface and adds a new convenience method, `\core\clock::time(): int`, to simplify replacement of the global `time()` method.
+The Moodle Clock extends the PSR-20 Clock Interface and adds the convenience method `\core\clock::time(): int` to simplify
+replacement of the global `time()` function.
-### Usage in standard classes {/* #usage-in-standard-classes */}
+### Usage via constructor injection {/* #usage-via-constructor-injection */}
-Where the calling code is not instantiated via Dependency Injection itself, the simplest way to fetch the clock is using `\core\di::get(\core\clock::class)`, for example:
+Declare the clock as a constructor dependency:
-```php title="Usage in legacy code"
-$clock = \core\di::get(\core\clock::class);
-
-// Fetch the current time as a \DateTimeImmutable.
-$clock->now();
-
-// Fetch the current time as a Unix Time Stamp.
-$clock->time();
-```
-
-### Usage via Constructor Injection {/* #usage-via-constructor-injection */}
-
-The recommended approach is to have the Dependency Injector inject into the constructor of a class.
-
-```php title="Usage in injected classes"
+```php title="Using an injected clock"
namespace mod_example;
class post {
public function __construct(
protected readonly \core\clock $clock,
protected readonly \moodle_database $db,
- )
+ ) {
+ }
public function create_thing(\stdClass $data): \stdClass {
$data->timecreated = $this->clock->time();
@@ -62,17 +51,41 @@ class post {
}
```
-When using DI to fetch the class, the dependencies will automatically added to the constructor arguments:
+At the application boundary, obtain the highest-level service. The container supplies its clock and database dependencies:
```php title="Obtaining the injected class"
-$post = \core\di::get(post::class);
+$post = \core\di::get(\mod_example\post::class);
```
+Do not call `\core\di::get(\core\clock::class)` from inside `post`. That hides the dependency and uses the container as a
+service locator.
+
+### Legacy and procedural boundaries {/* #usage-in-standard-classes */}
+
+Code which cannot receive constructor dependencies without a backwards-incompatible change may fetch the clock at its
+procedural or static boundary:
+
+```php title="Compatibility usage in legacy code"
+$clock = \core\di::get(\core\clock::class);
+
+// Fetch the current time as a \DateTimeImmutable.
+$clock->now();
+
+// Fetch the current time as a Unix timestamp.
+$clock->time();
+```
+
+Keep this lookup at the boundary. Pass the clock into any objects created below it.
+
## Unit testing {/* #unit-testing */}
-One of the most useful benefits to making consistent use of the Clock interface is to mock data within unit tests.
+One of the most useful benefits of consistently using the Clock interface is the ability to control time in unit tests.
-When testing code which makes use of the Clock interface, you can replace the standard system clock implementation with a testing clock which suits your needs.
+Calling either `advanced_testcase` helper described below performs the complete replacement: it creates a test clock, calls
+`\core\di::set(\core\clock::class, $clock)` to replace the container's clock for the test, and returns that same object. No
+additional container configuration is required. Any container-managed service resolved afterwards receives the replacement
+clock through constructor injection. This is an example of
+[replacing a dependency before obtaining the aggregate root](../di/index.md#unit-testing).
:::tip[Container Reset]
@@ -82,13 +95,10 @@ The DI container is automatically reset at the end of every test, which ensures
Moodle provides two standard test clocks, but you are welcome to create any other, as long as it implements the `\core\clock` interface.
-:::warning
-
-When mocking the clock, you _must_ do so _before_ fetching your service.
+:::warning[Call the helper before resolving the service]
-Any injected value within your service will persist for the lifetime of that service.
-
-Replacing the clock after fetching your service will have *no* effect.
+The helper call is the replacement step. Call it before obtaining the service from the container because replacing
+`\core\clock` does not rewrite the clock already stored in an existing service object.
:::
@@ -96,7 +106,8 @@ Replacing the clock after fetching your service will have *no* effect.
The incrementing clock increases the time by one second every time it is called. It can also be instantiated with a specific start time if preferred.
-A helper method, `mock_clock_with_incrementing(?int $starttime = null): \core\clock`, is provided within the standard testcase:
+The standard testcase provides
+`mock_clock_with_incrementing(?int $starttime = null): \incrementing_clock`:
```php title="Obtaining the incrementing clock"
class my_test extends \advanced_testcase {
@@ -104,9 +115,11 @@ class my_test extends \advanced_testcase {
// This class inserts data into the database.
$this->resetAfterTest(true);
+ // Create the test clock, replace \core\clock in the container, and return that replacement.
$clock = $this->mock_clock_with_incrementing();
- $post = \core\di::get(post::class);
+ // Because post is resolved afterwards, the container injects $clock into it.
+ $post = \core\di::get(\mod_example\post::class);
$posta = $post->create_thing((object) [
'name' => 'a',
]);
@@ -115,7 +128,7 @@ class my_test extends \advanced_testcase {
]);
// The incrementing clock automatically advanced by one second each time it is called.
- $this->assertGreaterThan($postb->timecreated, $posta->timecreated);
+ $this->assertGreaterThan($posta->timecreated, $postb->timecreated);
$this->assertLessThan($clock->time(), $postb->timecreated);
}
}
@@ -131,7 +144,7 @@ $clock = $this->mock_clock_with_incrementing(12345678);
The frozen clock uses a time which does not change, unless manually set. This can be useful when testing code which must handle time-based resolutions.
-A helper method, `mock_clock_with_frozen(?int $time = null): \core\clock`, is provided within the standard testcase:
+The standard testcase provides `mock_clock_with_frozen(?int $time = null): \frozen_clock`:
```php title="Obtaining and using the frozen clock"
class my_test extends \advanced_testcase {
@@ -141,7 +154,7 @@ class my_test extends \advanced_testcase {
$clock = $this->mock_clock_with_frozen();
- $post = \core\di::get(post::class);
+ $post = \core\di::get(\mod_example\post::class);
$posta = $post->create_thing((object) [
'name' => 'a',
]);
@@ -179,7 +192,7 @@ class my_test extends \advanced_testcase {
### Custom clock {/* #custom-clock */}
-If the standard cases are not suitable for you, then you can create a custom clock and inject it into the DI container.
+If the standard cases are not suitable, create a custom clock and register it with the DI container as a replacement.
```php title="Creating a custom clock"
class my_clock implements \core\clock {
@@ -191,7 +204,7 @@ class my_clock implements \core\clock {
public function now(): \DateTimeImmutable {
$time = new \DateTimeImmutable('@' . $this->time);
- $this->time = $this->time += 5;
+ $this->time += 5;
return $time;
}
@@ -203,10 +216,12 @@ class my_clock implements \core\clock {
class my_test extends \advanced_testcase {
public function test_my_thing(): void {
+ $this->resetAfterTest(true);
+
$clock = new my_clock();
- \core\di:set(\core\clock::class, $clock);
+ \core\di::set(\core\clock::class, $clock);
- $post = \core\di::get(post::class);
+ $post = \core\di::get(\mod_example\post::class);
$posta = $post->create_thing((object) [
'name' => 'a',
]);
diff --git a/docs/apis/core/di/index.md b/docs/apis/core/di/index.md
index 0529877c9..2c1b79f9b 100644
--- a/docs/apis/core/di/index.md
+++ b/docs/apis/core/di/index.md
@@ -5,225 +5,512 @@ tags:
- Container
- PSR-11
- PSR
-description: The use of PSR-11 compatible Dependency Injection in Moodle
+description: Using constructor injection and the Moodle dependency injection container
---
-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).
+Dependency injection means supplying an object with the dependencies that it needs, rather than having the object find or
+create those dependencies itself. In Moodle, dependencies should normally be passed through the constructor.
-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.
+The Dependency Injection (DI) container is a tool for assembling those objects. Moodle provides a
+[PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container through `\core\di`, backed by
+[PHP-DI](https://php-di.org). The container is not dependency injection itself.
-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.
+:::note[The important distinction]
-## Fetching dependencies {/* #fetching-dependencies */}
+Passing a dependency into a constructor is dependency injection. Calling `\core\di::get()` from inside the class that needs
+the dependency is service location.
-When accessing dependencies within a class, it is advisable to inject them into the constructor, for example:
+Use the container at a composition root to create the highest-level service or factory, then let constructor injection provide the rest
+of its object graph.
-```php title="Fetching a instance of the \core\http_client class from within a class"
-class my_thing {
+:::
+
+## Core concepts {/* #core-concepts */}
+
+- A **dependency** is a collaborator that an object needs to do its work, such as `\moodle_database` or `\core\clock`.
+- **Injection** is the act of supplying that collaborator from outside the object.
+- The **container** knows how to construct and reuse services and their dependencies.
+- An application **boundary** is the outermost code for one operation: the point where Moodle's runtime or a framework invokes
+ component code.
+- A **composition root** is the place where the operation's top-level objects and their dependency graphs are obtained or
+ assembled. A boundary may own composition itself, or the surrounding framework may do it.
+
+Runtime values are not services. User IDs, course IDs, event IDs, form data, and other per-operation values should be method or
+factory arguments. They must not be stored as mutable state in a shared container service.
+
+## Constructor injection {/* #constructor-injection */}
+
+Constructor injection is not a feature of Moodle's DI container. For required collaborators, it is a long-established PHP
+design practice that has been widely used for well over a decade. A class declares only its immediate dependencies and remains
+unaware of whether they were supplied by application code, a test, hand-written composition code, or a container. This makes
+the class easier to understand, reuse, and test. It also makes the class naturally compatible with Moodle's container, which
+can use its constructor types to assemble the object graph.
+
+The following example shows how this practice looks in a Moodle component. The repository contract and service live in
+separate autoloaded files:
+
+```php title="mod/example/classes/local/reminder_repository_interface.php"
+namespace mod_example\local;
+
+interface reminder_repository_interface {
+ public function create(int $userid, int $timecreated): void;
+}
+```
+
+```php title="mod/example/classes/local/reminder_service.php"
+namespace mod_example\local;
+
+final class reminder_service {
public function __construct(
- protected readonly \core\http_client $client,
+ private readonly reminder_repository_interface $repository,
+ private readonly \core\clock $clock,
) {
}
+
+ public function create_for_user(int $userid): void {
+ $this->repository->create(
+ userid: $userid,
+ timecreated: $this->clock->time(),
+ );
+ }
}
```
-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:
+Here `reminder_repository_interface` and `\core\clock` are dependencies. The `$userid` is runtime context, so it is passed to the
+operation.
+
+The constructor declares the contracts that the service needs; it does not select implementations for interfaces. Those
+mappings are [configured separately](#configuring-dependencies).
+
+[Constructor property promotion](https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion)
+and [readonly properties](https://www.php.net/manual/en/language.oop5.properties.php#language.oop5.properties.readonly-properties)
+make the dependency graph explicit and prevent dependencies from being replaced accidentally.
+
+With the interface mapping configured, Moodle's container can construct `reminder_service` and recursively supply its
+dependencies when application code requests the top-level service through
+`\core\di::get(\mod_example\local\reminder_service::class)`. The next section explains where such top-level requests belong.
+
+## Boundaries and composition roots {/* #composition-root */}
+
+Where `\core\di::get()` is called must be considered carefully. Moodle spans more than two decades of architectural styles,
+so its entry points are not all composed in the same way.
+
+Start by identifying the operation's application boundary: the outermost component code invoked for that operation. Common
+boundaries include route controller methods, external functions, hook callbacks, scheduled task `execute()` methods, and CLI
+or legacy PHP scripts. Runtime values such as validated request parameters and the current user ID enter at the boundary. It
+then invokes the top-level application objects for that operation. The classes called beneath it are inside the application's
+object graph; they are not boundaries merely because they call other classes.
+
+At that boundary, determine whether the surrounding framework already owns composition. Some framework-managed entry points
+are composed through the container. Others may be invoked without application dependencies being injected and must obtain
+their top-level application objects explicitly.
-```php title="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);
+When the surrounding framework does not inject the top-level objects, the boundary also owns composition. Fetch the
+highest-level service or factory there:
-// Fetching an instance of a class which is managed using DI.
-$thing = \core\di::get(my_thing::class);
+```php title="Composing the graph at an entry point"
+$service = \core\di::get(\mod_example\local\reminder_service::class);
+$service->create_for_user($USER->id);
```
-:::tip[Constructor Property Promotion and Readonly properties]
+Using the configured mappings for `reminder_repository_interface` and `\core\clock`, the container constructs
+`reminder_service` and supplies both dependencies to its constructor. Those collaborators can have their own constructor
+dependencies; application code does not need to fetch each one.
+
+This explicit lookup is appropriate at those boundaries because they are not currently invoked with injected application
+dependencies.
+
+### Framework-owned composition roots {/* #framework-owned-composition-roots */}
+
+The boundary and the composition root do not have to be the same piece of code. The
+[Routing subsystem](../../subsystems/routing/index.md) owns composition for routed web requests. It constructs the route
+controller through the container, so the controller can declare its highest-level service as an ordinary constructor
+dependency:
+
+```php title="A route controller composed by Moodle"
+final class reminder_controller {
+ public function __construct(
+ private readonly \mod_example\local\reminder_service $service,
+ ) {
+ }
+
+ #[\core\router\route(path: '/reminders', method: ['POST'])]
+ public function create(
+ \Psr\Http\Message\ResponseInterface $response,
+ ): \Psr\Http\Message\ResponseInterface {
+ global $USER;
+
+ $this->service->create_for_user($USER->id);
+ return $response;
+ }
+}
+```
-When using constructor-based injection, you can simplify your dependency injection by making use of [Constructor Property Promotion](https://stitcher.io/blog/constructor-promotion-in-php-8), and [Readonly properties](https://stitcher.io/blog/php-81-readonly-properties).
+Here, `create()` is the application boundary, while the router owns the composition root. The controller contains no container
+lookup because the router composes it and the complete dependency graph. A service needed by only one route may instead be
+declared as a typed route-method parameter; the router resolves those parameters through the container too. In both forms,
+the application classes below the controller continue to use ordinary constructor injection and know nothing about the
+container.
-The use of readonly properties is also highly recommended as it ensures that dependencies cannot be inadvertently changed.
+Avoid container lookups in the classes below that boundary:
-These language features are available in all Moodle versions supporting Dependency Injection.
+
```php
-class example_without_promotion {
- protected \core\http_client $client;
+final class reminder_service {
+ public function create_for_user(int $userid): void {
+ // Do not do this. The dependency is hidden and this class is coupled to the container.
+ $clock = \core\di::get(\core\clock::class);
+ }
+}
+```
+
+
+
+Constructor injection makes dependencies visible, keeps classes usable without Moodle's global container, and allows the
+container to replace a dependency throughout the graph.
+
+:::warning[Do not inject the container]
+
+Do not inject `\Psr\Container\ContainerInterface`, `\DI\Container`, or `\core\di`. Doing so turns the container into a
+service locator and hides the real dependencies of the class.
+
+The standard [`moodle` PHP CodeSniffer ruleset](/general/development/tools/phpcs) enforces this with
+`moodle.PHP.ForbiddenContainerInjection`. It reports an error when application code receives one of these container types;
+declare the service that the code actually needs instead.
+
+:::
+
+The recommended `moodle-extra` ruleset also enables `MoodleExtra.PHP.DiscouragedContainerLookup`. It reports a warning when
+a class calls `\core\di::get()` or `\core\di::get_container()`, because such calls usually indicate service location. If a
+class method is intentionally an entry-point composition root, keep the lookup at that boundary and document the exception
+with a targeted suppression:
+
+```php title="An intentional composition root inside a class"
+// phpcs:ignore MoodleExtra.PHP.DiscouragedContainerLookup.InClass -- Scheduled task composition root.
+$service = \core\di::get(\mod_example\local\reminder_service::class);
+```
+
+Classes below that boundary should declare their dependencies and should not suppress the warning.
+
+## Core example: the Calendar API {/* #calendar-example */}
+
+This section illustrates the architecture used inside Moodle core. Classes in the `core_calendar\local` namespace are internal
+to the Calendar component and are not public APIs for plugins.
+
+:::info[Dependency injection predates Moodle's core container]
+
+Calendar already used constructor injection in Moodle 3.3, seven years before Moodle 4.4 introduced `\core\di`. Work in
+[MDL-57442](https://moodle.atlassian.net/browse/MDL-57442) and
+[MDL-57750](https://moodle.atlassian.net/browse/MDL-57750), as part of the
+[MDL-55611](https://moodle.atlassian.net/browse/MDL-55611) project, created this dependency graph. The
+[hand-written composition code in Moodle 3.3](https://github.com/moodle/moodle/blob/v3.3.0/calendar/classes/local/event/container.php#L89-L183)
+constructed the event factories, mapper, vault, and retrieval strategy, passing dependencies into their constructors.
+
+That bespoke class was itself named `container` and exposed static accessors, so callers used it partly as a service locator.
+The graph it assembled still used dependency injection. The core DI container standardises and automates object assembly; it
+did not introduce the act of injecting dependencies into Moodle code.
+
+:::
+
+The Calendar API has a substantial dependency graph. Moodle-owned entry points obtain the event vault factory, provide the
+requesting user as runtime context, and then use the resulting vault for the operation:
+
+```php title="Using the Calendar API query root"
+$vaultfactory = \core\di::get(\core_calendar\local\event\data_access\event_vault_factory::class);
+$eventvault = $vaultfactory->create($USER->id);
+$events = $eventvault->get_events(
+ timestartfrom: $timestart,
+ timestartto: $timeend,
+);
+```
+
+The first line above is the query graph's only container lookup. It belongs in the Moodle-owned entry point, which is the
+composition root. The requested factory declares its dependencies as ordinary constructor arguments:
+```php title="Calendar event vault factory constructor"
+class event_vault_factory {
public function __construct(
- \core\http_client $client,
+ private readonly component_event_service $componenteventservice,
+ private readonly event_access_policy $eventaccesspolicy,
+ private readonly raw_event_retrieval_strategy_interface $retrievalstrategy,
) {
- $this->client = $client;
}
}
+```
+
+The same pattern continues further down the graph:
-class example_with_promotion {
+```php title="A dependency declaring its own contracts"
+class component_event_service {
public function __construct(
- protected readonly \core\http_client $client,
+ private readonly event_mapper_interface $mapper,
+ private readonly action_factory_interface $actionfactory,
) {
}
}
```
-:::
+Neither class calls the container or knows which implementations were configured. Each declares only the collaborators it
+needs and codes to their contracts. From the single `\core\di::get()` call at the boundary, the container follows those
+constructors recursively until it has assembled the complete graph.
+
+The container assembles the rest of the graph:
+
+```mermaid
+flowchart TD
+ boundary[Calendar entry point] --> vaultfactory[event_vault_factory]
+ vaultfactory -->|create requesting user ID| vault[event_vault]
+ vaultfactory --> componentservice[component_event_service]
+ vaultfactory --> policy[event_access_policy]
+ vaultfactory --> retrieval[raw_event_retrieval_strategy_interface]
+ componentservice --> mapper
+ componentservice --> actionfactory[action_factory_interface]
+ mapper --> entityfactory[event_entity_factory]
+ vault --> retrieval
+```
+
+`event_vault_factory::create()` accepts the requesting user ID and captures it for one vault. The ID is not retained by the
+container-managed factory. This matters because container services can live for the whole request and may be shared by several
+operations. Resolving `event_vault_interface` directly would either leave the container without the runtime user ID or hide that
+context in mutable global state.
+
+### A boundary that owns composition {/* #composition-boundary-example */}
+
+The external-services framework invokes `get_calendar_event_by_id()` as a static method and does not inject application
+dependencies. The method is therefore both the boundary and the composition root for that operation.
+
+The following shortened version shows all of its container lookups in context:
+
+```php title="Several top-level objects at one boundary"
+public static function get_calendar_event_by_id($eventid) {
+ global $PAGE, $USER;
+
+ // Start the query graph.
+ $eventvault = \core\di::get(
+ \core_calendar\local\event\data_access\event_vault_factory::class,
+ )->create($USER->id);
+ $event = $eventvault->get_event_by_id($eventid);
+ if (!$event) {
+ throw new \required_capability_exception(
+ \context_system::instance(),
+ 'moodle/course:view',
+ 'nopermissions',
+ 'error',
+ );
+ }
+
+ // Obtain a separate top-level collaborator used directly by this boundary.
+ $mapper = \core\di::get(
+ \core_calendar\local\event\mappers\event_mapper_interface::class,
+ );
+ if (!calendar_view_event_allowed($mapper->from_event_to_legacy_event($event))) {
+ throw new \moodle_exception('nopermissiontoviewcalendar', 'error');
+ }
+
+ $cache = new \core_calendar\external\events_related_objects_cache([$event]);
+ $relatedobjects = [
+ 'context' => $cache->get_context($event),
+ 'course' => $cache->get_course($event),
+ ];
+
+ // Start the independent output graph.
+ $outputfactory = \core\di::get(
+ \core_calendar\local\output\event_exporter_factory::class,
+ );
+ $exporter = $outputfactory->create_event_exporter($event, $relatedobjects);
+
+ return $exporter->export($PAGE->get_renderer('core_calendar'));
+}
+```
+
+The three lookups obtain top-level capabilities used directly by the entry point:
+
+- `event_vault_factory` starts the query graph and receives the requesting user as runtime context.
+- `event_mapper_interface` converts the event for a legacy permission check performed by the boundary.
+- `event_exporter_factory` starts the output graph.
+
+The goal is not mechanically limiting every request to one `\core\di::get()` call. A boundary can obtain more than one
+top-level service or factory when it directly coordinates independent parts of an operation. It should not obtain the
+dependencies of those objects: their constructors declare those dependencies and the container supplies them. If one
+application service naturally owns the complete use case, obtain that single service. Do not introduce a facade whose only
+purpose is to hide several otherwise legitimate boundary lookups.
+
+The `event_exporter_factory` itself is added to the exporter's optional related data and passed to nested exporters. Leaf
+exporters call the supplied factory to map events and create human-readable dates; they do not fetch those collaborators from
+the container.
+
+```mermaid
+flowchart TD
+ outputboundary[Calendar output boundary] --> outputfactory[event_exporter_factory]
+ outputfactory --> outputmapper[event_mapper_interface]
+ outputfactory --> periodfactory[humantimeperiod_factory]
+ periodfactory --> datefactory[humandate_factory]
+ datefactory --> clock[core clock]
+ outputfactory --> rootexporter[Root exporter]
+ rootexporter --> nestedexporters[Nested exporters]
+ rootexporter -. related data .-> outputfactory
+ nestedexporters -. related data .-> outputfactory
+```
+
+Existing directly constructed exporters use an isolated compatibility path, while the normal graph does not perform
+container lookups in leaf exporters.
+
+## Autowiring {/* #autowiring */}
+
+Autowiring means inspecting the constructor of a known concrete class and resolving its declared dependencies. It does not
+mean searching the codebase for a class which implements an interface.
+
+When the container is asked for a concrete class, it can normally construct that class without configuration:
+
+```php
+final class reminder_repository implements reminder_repository_interface {
+ public function __construct(
+ private readonly \moodle_database $db,
+ ) {
+ }
+
+ public function create(int $userid, int $timecreated): void {
+ $this->db->insert_record('example_reminders', (object) [
+ 'userid' => $userid,
+ 'timecreated' => $timecreated,
+ ]);
+ }
+}
+```
+
+The container can construct `reminder_repository` without extra instructions from the component: it inspects the constructor,
+sees `\moodle_database`, and Moodle core already knows how to provide it. The fact that `reminder_repository` implements
+`reminder_repository_interface` does not create a reverse mapping from the interface to this implementation.
-## Configuring dependencies {/* #configuring-dependencies */}
+A request for `reminder_repository_interface::class` must first be mapped to an implementation. The
+[configuration section below](#configuring-dependencies) explains how a component supplies that mapping. Once the mapping is
+configured, autowiring takes over: the container follows it to `reminder_repository`, inspects its constructor, and builds its
+dependencies recursively.
-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.
+## Configuring interfaces and complex services {/* #configuring-dependencies */}
-
+The interface-to-implementation mapping described above is a container definition. A definition tells the container how to
+resolve an entry which autowiring cannot decide or construct by itself. Moodle core supplies the definition for
+`\moodle_database` used in the previous example. Components contribute their own definitions with the
+`\core\hook\di_configuration` hook. Choosing an implementation for an interface is the most common example; definitions can
+also supply scalar configuration or delegate construction to a legacy factory.
-
+Container entries are identified by a string ID, normally a fully-qualified class or interface name. Arbitrary string IDs are
+strongly discouraged because they hide the type relationship.
-The callback must be linked to the hook by specifying a callback in the plugin's `hooks.php` file:
+Register the hook callback in the component's `db/hooks.php`:
```php title="mod/example/db/hooks.php"
\core\hook\di_configuration::class,
- 'callback' => \mod_example\hook_listener::class . '::inject_dependencies',
+ 'callback' => [\mod_example\hook_callbacks::class, 'provide_di_configuration'],
],
];
```
-
+Map the interface to a concrete implementation in the callback:
-
-
-The hook listener consists of a static method on a class.
-
-```php title="mod/example/classes/hook_listener.php"
+```php title="mod/example/classes/hook_callbacks.php"
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,
- );
- }
- )
+ reminder_repository_interface::class,
+ \DI\get(reminder_repository::class),
+ );
}
}
```
-
+Consumers continue to declare the interface in their constructors. They do not need to know which implementation was
+configured.
-
+## Unit testing {/* #unit-testing */}
-## Mocking dependencies in Unit Tests {/* #mocking-dependencies-in-unit-tests */}
+Constructor injection makes small unit tests possible without a container: instantiate the class and pass stubs or mocks to
+its constructor.
-One of the most convenient features of Dependency Injection is the ability to provide a mocked version of the dependency during unit testing.
+```php title="Testing a service without a container"
+public function test_reminder_uses_the_repository(): void {
+ $repository = $this->createMock(reminder_repository_interface::class);
+ $repository->expects($this->once())
+ ->method('create')
+ ->with(42, 1234567890);
-Moodle resets the Dependency Injection Container between each unit test, which means that little-to-no cleanup is required.
+ $clock = $this->createStub(\core\clock::class);
+ $clock->method('time')
+ ->willReturn(1234567890);
-```php title="Injecting a Mocked dependency"
- '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());
- }
+ $service = new reminder_service(
+ repository: $repository,
+ clock: $clock,
+ );
+ $service->create_for_user(42);
}
```
-## Injecting dependencies {/* #injecting-dependencies */}
+This test has no container setup: the constructor receives the two dependencies directly.
-Dependencies can be usually be easily injected into classes which are themselves loaded using Dependency Injection.
+For tests which exercise a complete object graph, replace a dependency before obtaining the aggregate root:
-In most cases in Moodle, this should be via the class constructor, for example:
+```php title="Replacing a dependency in a container-backed test"
+public function test_reminder_uses_the_repository(): void {
+ $repository = $this->createMock(reminder_repository_interface::class);
+ $repository->expects($this->once())
+ ->method('create');
-```php title="Injecting via the constructor"
-class thing_manager {
- public function __construct(
- protected readonly \moodle_database $db,
- ) {
- }
+ \core\di::set(reminder_repository_interface::class, $repository);
- public function get_things(): array {
- return $this->db->get_records('example_things');
- }
+ // Fetch the root only after all replacements have been configured.
+ $service = \core\di::get(reminder_service::class);
+ $service->create_for_user(42);
}
+```
-// Fetching the injected class from legacy code:
-$manager = \core\di::get(thing_manager::class);
-$things = $manager->get_things();
+The [Clock API test helpers](../clock/index.md#unit-testing) wrap this container-backed pattern for `\core\clock`: they
+install a replacement before the root is resolved and return that same clock object to the test.
-// 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();
- }
-}
-```
+Moodle resets the DI container between PHPUnit tests.
-:::warning[A note on injecting the Container]
+:::warning[Replace dependencies before fetching the root]
-It is generally inadvisable to inject the Container itself. Please do not inject the `\Psr\Container\ContainerInterface`.
+Container-managed services can be shared for the lifetime of the container. Replacing a dependency after its consumer has
+already been constructed does not rewrite the existing object.
:::
-## Advanced usage {/* #advanced-usage */}
+## Legacy and static entry points {/* #legacy-entry-points */}
-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.
+Some procedural APIs and static factory methods cannot receive constructor dependencies without a backwards-incompatible
+signature change. A container lookup may be retained at that boundary as a compatibility bridge.
-### Resetting the Container {/* #resetting-the-container */}
+Keep such bridges small and explicit. New code should enter through an injected service or factory, and the compatibility
+lookup should not be copied into the normal object graph.
-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.
+## Advanced usage {/* #advanced-usage */}
-:::tip[Unit testing]
+All access to the active container should use `\core\di`. Application code should not access the underlying PHP-DI container
+directly.
-The container is already reset after each test when running unit tests. It is not necessary nor recommended to so manually.
+### Resetting the container {/* #resetting-the-container */}
-:::
+The container is normally instantiated during bootstrap and lives for the request. Resetting it in application code can leave
+existing objects holding dependencies from an older graph.
-```php title="Resetting the Container"
+```php title="Resetting the container"
\core\di::reset_container();
```
-:::danger
-
-Resetting an actively-used container can lead to unintended consequences.
-
-:::
+This is intended for infrastructure and specialised test scenarios. PHPUnit already resets the container between tests, so
+tests should not normally call it themselves.
diff --git a/docs/apis/subsystems/routing/index.md b/docs/apis/subsystems/routing/index.md
index 38b150921..825f9a2e0 100644
--- a/docs/apis/subsystems/routing/index.md
+++ b/docs/apis/subsystems/routing/index.md
@@ -23,7 +23,11 @@ Routes may optionally describe additional metadata including:
- Responses
- Examples
-When invoked, any parameter to the route method will be resolved using a combination of [Dependency Injection](../../core/di/index.md) and resolution of path, query, and header parameters.
+The Routing subsystem is a framework-owned [composition root](../../core/di/index.md#composition-root). It constructs route
+classes through the Dependency Injection container. Dependencies used throughout the class should normally be declared in
+its constructor; a dependency needed by only one route may instead be a typed route-method parameter. When invoked, the
+router resolves those service parameters alongside path, query, and header parameters. Route code therefore does not need to
+call `\core\di::get()`.
## Using the `route` attribute {/* #using-the-route-attribute */}