You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/apis/core/clock/index.md
+44-39Lines changed: 44 additions & 39 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,7 +11,7 @@ description: Fetching the current time
11
11
12
12
<Sinceversion="4.4"issueNumber="MDL-80838" />
13
13
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.
15
15
16
16
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.
17
17
@@ -23,34 +23,21 @@ We recommend that the Clock Interface is used consistently in your code instead
23
23
24
24
## Usage {/* #usage */}
25
25
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.
27
27
28
-
### Usage in standard classes {/* #usage-in-standard-classes*/}
28
+
### Usage via constructor injection {/* #usage-via-constructor-injection*/}
29
29
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:
31
31
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"
47
33
namespace mod_example;
48
34
49
35
class post {
50
36
public function __construct(
51
37
protected readonly \core\clock $clock,
52
38
protected readonly \moodle_database $db,
53
-
)
39
+
) {
40
+
}
54
41
55
42
public function create_thing(\stdClass $data): \stdClass {
56
43
$data->timecreated = $this->clock->time();
@@ -62,17 +49,35 @@ class post {
62
49
}
63
50
```
64
51
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:
66
53
67
54
```php title="Obtaining the injected class"
68
-
$post = \core\di::get(post::class);
55
+
$post = \core\di::get(\mod_example\post::class);
69
56
```
70
57
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
+
71
76
## Unit testing {/* #unit-testing */}
72
77
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.
74
79
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).
76
81
77
82
:::tip[Container Reset]
78
83
@@ -82,31 +87,29 @@ The DI container is automatically reset at the end of every test, which ensures
82
87
83
88
Moodle provides two standard test clocks, but you are welcome to create any other, as long as it implements the `\core\clock` interface.
84
89
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]
88
91
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.
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.
133
136
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`:
135
138
136
139
```php title="Obtaining and using the frozen clock"
137
140
class my_test extends \advanced_testcase {
@@ -141,7 +144,7 @@ class my_test extends \advanced_testcase {
141
144
142
145
$clock = $this->mock_clock_with_frozen();
143
146
144
-
$post = \core\di::get(post::class);
147
+
$post = \core\di::get(\mod_example\post::class);
145
148
$posta = $post->create_thing((object) [
146
149
'name' => 'a',
147
150
]);
@@ -179,7 +182,7 @@ class my_test extends \advanced_testcase {
179
182
180
183
### Custom clock {/* #custom-clock */}
181
184
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.
183
186
184
187
```php title="Creating a custom clock"
185
188
class my_clock implements \core\clock {
@@ -191,7 +194,7 @@ class my_clock implements \core\clock {
191
194
192
195
public function now(): \DateTimeImmutable {
193
196
$time = new \DateTimeImmutable('@' . $this->time);
194
-
$this->time = $this->time += 5;
197
+
$this->time += 5;
195
198
196
199
return $time;
197
200
}
@@ -203,10 +206,12 @@ class my_clock implements \core\clock {
0 commit comments