Skip to content

Commit aaf8659

Browse files
committed
[docs] Add docs on usage of PSR-20 \core\clock
1 parent 2c06b11 commit aaf8659

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed

docs/apis/core/clock/index.md

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
---
2+
title: Clock
3+
tags:
4+
- Time
5+
- PSR-20
6+
- PSR
7+
- Unit testing
8+
- Testing
9+
description: Fetching the current time
10+
---
11+
12+
import {
13+
Since,
14+
ValidExample,
15+
InvalidExample,
16+
} from '@site/src/components';
17+
18+
<Since version="4.4" issueNumber="MDL-80838" />
19+
20+
Moodle supports use of a [PSR-20](https://php-fig.org/psr/psr20/) compatible Clock interface, which should be accessed using Dependency Injection.
21+
22+
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+
24+
:::tip Recommended usage
25+
26+
We recommend that the Clock Interface is used consistently in your code instead of using the standard `time()` method.
27+
28+
:::
29+
30+
## Usage
31+
32+
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.
33+
34+
### Usage in standard classes
35+
36+
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:
37+
38+
```php title="Usage in legacy code"
39+
$clock = \core\di::get(\core\clock::class);
40+
41+
// Fetch the current time as a \DateTimeImmutable.
42+
$clock->now();
43+
44+
// Fetch the current time as a Unix Time Stamp.
45+
$clock->time();
46+
```
47+
48+
### Usage via Constructor Injection
49+
50+
The recommended approach is to have the Dependency Injector inject into the constructor of a class.
51+
52+
```php title="Usage in injected classes"
53+
namespace mod_example;
54+
55+
class post {
56+
public function __construct(
57+
protected readonly \core\clock $clock,
58+
protected readonly \moodle_database $db,
59+
)
60+
61+
public function create_thing(\stdClass $data): \stdClass {
62+
$data->timecreated = $this->clock->time();
63+
64+
$data->id = $this->db->insert_record('example_thing', $data);
65+
66+
return $data;
67+
}
68+
}
69+
```
70+
71+
When using DI to fetch the class, the dependencies will automatically added to the constructor arguments:
72+
73+
```php title="Obtaining the injected class"
74+
$post = \core\di::get(post::class);
75+
```
76+
77+
### Usage via Injection Attributes
78+
79+
In some cases it may be more appropriate to inject dependencies using the `\DI\Attribute\Inject` attribute.
80+
81+
:::warning Usage of Injection via Attributes
82+
83+
The [Best Practices](https://php-di.org/doc/best-practices.html) guide for PHP-DI recommends that Attribute-based injection should only be used by Controllers.
84+
85+
Whilst Moodle does not currently have frontend controllers as a concept, these are expected to arrive in the near future.
86+
87+
Attribute-based injection can also useful when retrofitting legacy code.
88+
89+
:::
90+
91+
```php title="Usage in injected classes"
92+
namespace mod_example\route;
93+
94+
use mod_example\post;
95+
use DI\Attribute\Inject;
96+
use Psr\Http\Message\ResponseInterface;
97+
use Psr\Http\Message\RequestInterface;
98+
99+
class post_controller {
100+
/** @var \core\clock The Moodle Clock */
101+
#[Inject]
102+
protected \core\clock $clock;
103+
104+
/** @var post */
105+
#[Inject]
106+
protected post $post;
107+
108+
109+
public function create_thing(RequestInterface $request): ResponseInterface {
110+
$data->timecreated = $this->clock->time();
111+
$this->post->create_thing($data);
112+
113+
// ...
114+
}
115+
}
116+
```
117+
118+
## Unit testing
119+
120+
One of the most useful benefits to making consistent use of the Clock interface is to mock data within unit tests.
121+
122+
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.
123+
124+
:::tip Container Reset
125+
126+
The DI container is automatically reset at the end of every test, which ensures that your clock does not bleed into subsequent tests.
127+
128+
:::
129+
130+
Moodle provides two standard test clocks, but you are welcome to create any other, as long as it implements the `\core\clock` interface.
131+
132+
:::warning
133+
134+
When mocking the clock, you _must_ do so _before_ fetching your service.
135+
136+
Any injected value within your service will persist for the lifetime of that service.
137+
138+
Replacing the clock after fetching your service will have *no* effect.
139+
140+
:::
141+
142+
### Incrementing clock
143+
144+
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.
145+
146+
A helper method, `mock_clock_with_incrementing(?int $starttime = null): \core\clock`, is provided within the standard testcase:
147+
148+
```php title="Obtaining the incrementing clock"
149+
class my_test extends \advanced_testcase {
150+
public function test_create_thing(): void {
151+
// This class inserts data into the database.
152+
$this->resetAfterTest(true);
153+
154+
$clock = $this->mock_clock_with_incrementing();
155+
156+
$post = \core\di::get(post::class);
157+
$posta = $post->create_thing((object) [
158+
'name' => 'a',
159+
]);
160+
$postb = $post->create_thing((object) [
161+
'name' => 'a',
162+
]);
163+
164+
// The incrementing clock automatically advanced by one second each time it is called.
165+
$this->assertGreaterThan($postb->timecreated, $posta->timecreated);
166+
$this->assertLessThan($clock->time(), $postb->timecreated);
167+
}
168+
}
169+
```
170+
171+
It is also possible to specify a start time for the clock;
172+
173+
```php title="Setting the start time"
174+
$clock = $this->mock_clock_with_incrementing(12345678);
175+
```
176+
177+
### Frozen clock
178+
179+
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.
180+
181+
A helper method, `mock_clock_with_frozen(?int $time = null): \core\clock`, is provided within the standard testcase:
182+
183+
```php title="Obtaining and using the frozen clock"
184+
class my_test extends \advanced_testcase {
185+
public function test_create_thing(): void {
186+
// This class inserts data into the database.
187+
$this->resetAfterTest(true);
188+
189+
$clock = $this->mock_clock_with_frozen();
190+
191+
$post = \core\di::get(post::class);
192+
$posta = $post->create_thing((object) [
193+
'name' => 'a',
194+
]);
195+
$postb = $post->create_thing((object) [
196+
'name' => 'a',
197+
]);
198+
199+
// The frozen clock keeps the same time.
200+
$this->assertEquals($postb->timecreated, $posta->timecreated);
201+
$this->assertEquals($clock->time(), $postb->timecreated);
202+
203+
// The time can be manually set.
204+
$clock->set_to(12345678);
205+
$postc = $post->create_thing((object) [
206+
'name' => 'a',
207+
]);
208+
209+
// The frozen clock keeps the same time.
210+
$this->assertEquals(12345678, $postc->timecreated);
211+
212+
// And can also be bumped.
213+
$clock->set_to(0);
214+
$this->assertEquals(0, $clock->time());
215+
216+
// Bump the current time by 1 second.
217+
$clock->bump();
218+
$this->assertEquals(1, $clock->time());
219+
220+
// Bump by 4 seconds.
221+
$clock->bump(4);
222+
$this->assertEquals(5, $clock->time());
223+
}
224+
}
225+
```
226+
227+
### Custom clock
228+
229+
If the standard cases are not suitable for you, then you can create a custom clock and inject it into the DI container.
230+
231+
```php title="Creating a custom clock"
232+
class my_clock implements \core\clock {
233+
public int $time;
234+
235+
public function __construct() {
236+
$this->time = time();
237+
}
238+
239+
public function now(): \DateTimeImmutable {
240+
$time = new \DateTimeImmutable('@' . $this->time);
241+
$this->time = $this->time += 5;
242+
243+
return $time;
244+
}
245+
246+
public function time(): int {
247+
return $this->now()->getTimestamp();
248+
}
249+
}
250+
251+
class my_test extends \advanced_testcase {
252+
public function test_my_thing(): void {
253+
$clock = new my_clock();
254+
\core\di:set(\core\clock::class, $clock);
255+
256+
$post = \core\di::get(post::class);
257+
$posta = $post->create_thing((object) [
258+
'name' => 'a',
259+
]);
260+
}
261+
}
262+
```

docs/devupdate.md

+8
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ $formatter->format_text(
154154

155155
:::
156156

157+
### Clock interface
158+
159+
<Since version="4.4" issueNumber="MDL-80838" />
160+
161+
Moodle now supports use of a PSR-20 compliant Clock Interface, accessed via Dependency Injection.
162+
163+
See the [detailed documentation](./apis/core/clock/index.md) on how to use this new interface.
164+
157165
## Enrolment
158166

159167
### Support for multiple instances in csv course upload

0 commit comments

Comments
 (0)