Skip to content

Commit 86534a4

Browse files
authored
Merge pull request #886 from andrewnicols/mdl80838
Documentation for \core\clock interface
2 parents bbec2b3 + fc18f77 commit 86534a4

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-0
lines changed

docs/apis/core/clock/index.md

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
## Unit testing
78+
79+
One of the most useful benefits to making consistent use of the Clock interface is to mock data within unit tests.
80+
81+
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.
82+
83+
:::tip Container Reset
84+
85+
The DI container is automatically reset at the end of every test, which ensures that your clock does not bleed into subsequent tests.
86+
87+
:::
88+
89+
Moodle provides two standard test clocks, but you are welcome to create any other, as long as it implements the `\core\clock` interface.
90+
91+
:::warning
92+
93+
When mocking the clock, you _must_ do so _before_ fetching your service.
94+
95+
Any injected value within your service will persist for the lifetime of that service.
96+
97+
Replacing the clock after fetching your service will have *no* effect.
98+
99+
:::
100+
101+
### Incrementing clock
102+
103+
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.
104+
105+
A helper method, `mock_clock_with_incrementing(?int $starttime = null): \core\clock`, is provided within the standard testcase:
106+
107+
```php title="Obtaining the incrementing clock"
108+
class my_test extends \advanced_testcase {
109+
public function test_create_thing(): void {
110+
// This class inserts data into the database.
111+
$this->resetAfterTest(true);
112+
113+
$clock = $this->mock_clock_with_incrementing();
114+
115+
$post = \core\di::get(post::class);
116+
$posta = $post->create_thing((object) [
117+
'name' => 'a',
118+
]);
119+
$postb = $post->create_thing((object) [
120+
'name' => 'a',
121+
]);
122+
123+
// The incrementing clock automatically advanced by one second each time it is called.
124+
$this->assertGreaterThan($postb->timecreated, $posta->timecreated);
125+
$this->assertLessThan($clock->time(), $postb->timecreated);
126+
}
127+
}
128+
```
129+
130+
It is also possible to specify a start time for the clock;
131+
132+
```php title="Setting the start time"
133+
$clock = $this->mock_clock_with_incrementing(12345678);
134+
```
135+
136+
### Frozen clock
137+
138+
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.
139+
140+
A helper method, `mock_clock_with_frozen(?int $time = null): \core\clock`, is provided within the standard testcase:
141+
142+
```php title="Obtaining and using the frozen clock"
143+
class my_test extends \advanced_testcase {
144+
public function test_create_thing(): void {
145+
// This class inserts data into the database.
146+
$this->resetAfterTest(true);
147+
148+
$clock = $this->mock_clock_with_frozen();
149+
150+
$post = \core\di::get(post::class);
151+
$posta = $post->create_thing((object) [
152+
'name' => 'a',
153+
]);
154+
$postb = $post->create_thing((object) [
155+
'name' => 'a',
156+
]);
157+
158+
// The frozen clock keeps the same time.
159+
$this->assertEquals($postb->timecreated, $posta->timecreated);
160+
$this->assertEquals($clock->time(), $postb->timecreated);
161+
162+
// The time can be manually set.
163+
$clock->set_to(12345678);
164+
$postc = $post->create_thing((object) [
165+
'name' => 'a',
166+
]);
167+
168+
// The frozen clock keeps the same time.
169+
$this->assertEquals(12345678, $postc->timecreated);
170+
171+
// And can also be bumped.
172+
$clock->set_to(0);
173+
$this->assertEquals(0, $clock->time());
174+
175+
// Bump the current time by 1 second.
176+
$clock->bump();
177+
$this->assertEquals(1, $clock->time());
178+
179+
// Bump by 4 seconds.
180+
$clock->bump(4);
181+
$this->assertEquals(5, $clock->time());
182+
}
183+
}
184+
```
185+
186+
### Custom clock
187+
188+
If the standard cases are not suitable for you, then you can create a custom clock and inject it into the DI container.
189+
190+
```php title="Creating a custom clock"
191+
class my_clock implements \core\clock {
192+
public int $time;
193+
194+
public function __construct() {
195+
$this->time = time();
196+
}
197+
198+
public function now(): \DateTimeImmutable {
199+
$time = new \DateTimeImmutable('@' . $this->time);
200+
$this->time = $this->time += 5;
201+
202+
return $time;
203+
}
204+
205+
public function time(): int {
206+
return $this->now()->getTimestamp();
207+
}
208+
}
209+
210+
class my_test extends \advanced_testcase {
211+
public function test_my_thing(): void {
212+
$clock = new my_clock();
213+
\core\di:set(\core\clock::class, $clock);
214+
215+
$post = \core\di::get(post::class);
216+
$posta = $post->create_thing((object) [
217+
'name' => 'a',
218+
]);
219+
}
220+
}
221+
```

docs/devupdate.md

+8
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,14 @@ This functionality is intended to simplify deprecation of features such as const
422422

423423
This functionality does not replace the phpdoc `@deprecated` docblock.
424424

425+
### Clock interface
426+
427+
<Since version="4.4" issueNumber="MDL-80838" />
428+
429+
Moodle now supports use of a PSR-20 compliant Clock Interface, accessed via Dependency Injection.
430+
431+
See the [detailed documentation](./apis/core/clock/index.md) on how to use this new interface.
432+
425433
## Enrolment
426434

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

0 commit comments

Comments
 (0)