Skip to content

Commit 116e82a

Browse files
committed
[docs] Improve dependency injection guidance
Clarify constructor injection, autowiring, container definitions, and application boundaries, using Calendar as a canonical core example.
1 parent 6347680 commit 116e82a

3 files changed

Lines changed: 406 additions & 168 deletions

File tree

‎docs/apis/core/clock/index.md‎

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ description: Fetching the current time
1111

1212
<Since version="4.4" issueNumber="MDL-80838" />
1313

14-
Moodle supports use of a [PSR-20](https://php-fig.org/psr/psr-20/) compatible Clock interface, which should be accessed using Dependency Injection.
14+
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.
1515

1616
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.
1717

@@ -23,34 +23,21 @@ We recommend that the Clock Interface is used consistently in your code instead
2323

2424
## Usage {/* #usage */}
2525

26-
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.
26+
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.
2727

28-
### Usage in standard classes {/* #usage-in-standard-classes */}
28+
### Usage via constructor injection {/* #usage-via-constructor-injection */}
2929

30-
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:
30+
Declare the clock as a constructor dependency:
3131

32-
```php title="Usage in legacy code"
33-
$clock = \core\di::get(\core\clock::class);
34-
35-
// Fetch the current time as a \DateTimeImmutable.
36-
$clock->now();
37-
38-
// Fetch the current time as a Unix Time Stamp.
39-
$clock->time();
40-
```
41-
42-
### Usage via Constructor Injection {/* #usage-via-constructor-injection */}
43-
44-
The recommended approach is to have the Dependency Injector inject into the constructor of a class.
45-
46-
```php title="Usage in injected classes"
32+
```php title="Using an injected clock"
4733
namespace mod_example;
4834

4935
class post {
5036
public function __construct(
5137
protected readonly \core\clock $clock,
5238
protected readonly \moodle_database $db,
53-
)
39+
) {
40+
}
5441

5542
public function create_thing(\stdClass $data): \stdClass {
5643
$data->timecreated = $this->clock->time();
@@ -62,17 +49,35 @@ class post {
6249
}
6350
```
6451

65-
When using DI to fetch the class, the dependencies will automatically added to the constructor arguments:
52+
At the application boundary, obtain the highest-level service. The container supplies its clock and database dependencies:
6653

6754
```php title="Obtaining the injected class"
68-
$post = \core\di::get(post::class);
55+
$post = \core\di::get(\mod_example\post::class);
6956
```
7057

58+
Do not call `\core\di::get(\core\clock::class)` from inside `post`. That hides the dependency and uses the container as a service locator.
59+
60+
### Legacy and procedural boundaries {/* #usage-in-standard-classes */}
61+
62+
Code which cannot receive constructor dependencies without a backwards-incompatible change may fetch the clock at its procedural or static boundary:
63+
64+
```php title="Compatibility usage in legacy code"
65+
$clock = \core\di::get(\core\clock::class);
66+
67+
// Fetch the current time as a \DateTimeImmutable.
68+
$clock->now();
69+
70+
// Fetch the current time as a Unix timestamp.
71+
$clock->time();
72+
```
73+
74+
Keep this lookup at the boundary. Pass the clock into any objects created below it.
75+
7176
## Unit testing {/* #unit-testing */}
7277

73-
One of the most useful benefits to making consistent use of the Clock interface is to mock data within unit tests.
78+
One of the most useful benefits of consistently using the Clock interface is the ability to control time in unit tests.
7479

75-
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.
80+
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).
7681

7782
:::tip[Container Reset]
7883

@@ -82,31 +87,29 @@ The DI container is automatically reset at the end of every test, which ensures
8287

8388
Moodle provides two standard test clocks, but you are welcome to create any other, as long as it implements the `\core\clock` interface.
8489

85-
:::warning
86-
87-
When mocking the clock, you _must_ do so _before_ fetching your service.
90+
:::warning[Call the helper before resolving the service]
8891

89-
Any injected value within your service will persist for the lifetime of that service.
90-
91-
Replacing the clock after fetching your service will have *no* effect.
92+
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.
9293

9394
:::
9495

9596
### Incrementing clock {/* #incrementing-clock */}
9697

9798
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.
9899

99-
A helper method, `mock_clock_with_incrementing(?int $starttime = null): \core\clock`, is provided within the standard testcase:
100+
The standard testcase provides `mock_clock_with_incrementing(?int $starttime = null): \incrementing_clock`:
100101

101102
```php title="Obtaining the incrementing clock"
102103
class my_test extends \advanced_testcase {
103104
public function test_create_thing(): void {
104105
// This class inserts data into the database.
105106
$this->resetAfterTest(true);
106107

108+
// Create the test clock, replace \core\clock in the container, and return that replacement.
107109
$clock = $this->mock_clock_with_incrementing();
108110

109-
$post = \core\di::get(post::class);
111+
// Because post is resolved afterwards, the container injects $clock into it.
112+
$post = \core\di::get(\mod_example\post::class);
110113
$posta = $post->create_thing((object) [
111114
'name' => 'a',
112115
]);
@@ -115,7 +118,7 @@ class my_test extends \advanced_testcase {
115118
]);
116119

117120
// The incrementing clock automatically advanced by one second each time it is called.
118-
$this->assertGreaterThan($postb->timecreated, $posta->timecreated);
121+
$this->assertGreaterThan($posta->timecreated, $postb->timecreated);
119122
$this->assertLessThan($clock->time(), $postb->timecreated);
120123
}
121124
}
@@ -131,7 +134,7 @@ $clock = $this->mock_clock_with_incrementing(12345678);
131134

132135
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.
133136

134-
A helper method, `mock_clock_with_frozen(?int $time = null): \core\clock`, is provided within the standard testcase:
137+
The standard testcase provides `mock_clock_with_frozen(?int $time = null): \frozen_clock`:
135138

136139
```php title="Obtaining and using the frozen clock"
137140
class my_test extends \advanced_testcase {
@@ -141,7 +144,7 @@ class my_test extends \advanced_testcase {
141144

142145
$clock = $this->mock_clock_with_frozen();
143146

144-
$post = \core\di::get(post::class);
147+
$post = \core\di::get(\mod_example\post::class);
145148
$posta = $post->create_thing((object) [
146149
'name' => 'a',
147150
]);
@@ -179,7 +182,7 @@ class my_test extends \advanced_testcase {
179182

180183
### Custom clock {/* #custom-clock */}
181184

182-
If the standard cases are not suitable for you, then you can create a custom clock and inject it into the DI container.
185+
If the standard cases are not suitable, create a custom clock and register it with the DI container as a replacement.
183186

184187
```php title="Creating a custom clock"
185188
class my_clock implements \core\clock {
@@ -191,7 +194,7 @@ class my_clock implements \core\clock {
191194

192195
public function now(): \DateTimeImmutable {
193196
$time = new \DateTimeImmutable('@' . $this->time);
194-
$this->time = $this->time += 5;
197+
$this->time += 5;
195198

196199
return $time;
197200
}
@@ -203,10 +206,12 @@ class my_clock implements \core\clock {
203206

204207
class my_test extends \advanced_testcase {
205208
public function test_my_thing(): void {
209+
$this->resetAfterTest(true);
210+
206211
$clock = new my_clock();
207-
\core\di:set(\core\clock::class, $clock);
212+
\core\di::set(\core\clock::class, $clock);
208213

209-
$post = \core\di::get(post::class);
214+
$post = \core\di::get(\mod_example\post::class);
210215
$posta = $post->create_thing((object) [
211216
'name' => 'a',
212217
]);

0 commit comments

Comments
 (0)