Skip to content

Commit 3b8186e

Browse files
[Clock] Add Clock class and now() function
1 parent 9d798b3 commit 3b8186e

File tree

7 files changed

+246
-0
lines changed

7 files changed

+246
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `ClockAwareTrait` to help write time-sensitive classes
8+
* Add `Clock` class and `now()` function
89

910
6.2
1011
---

Clock.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Clock;
13+
14+
use Psr\Clock\ClockInterface as PsrClockInterface;
15+
16+
/**
17+
* A global clock.
18+
*
19+
* @author Nicolas Grekas <[email protected]>
20+
*/
21+
final class Clock implements ClockInterface
22+
{
23+
private static ClockInterface $globalClock;
24+
25+
public function __construct(
26+
private readonly ?PsrClockInterface $clock = null,
27+
private ?\DateTimeZone $timezone = null,
28+
) {
29+
}
30+
31+
/**
32+
* Returns the current global clock.
33+
*
34+
* Note that you should prefer injecting a ClockInterface or using
35+
* ClockAwareTrait when possible instead of using this method.
36+
*/
37+
public static function get(): ClockInterface
38+
{
39+
return self::$globalClock ??= new NativeClock();
40+
}
41+
42+
public static function set(PsrClockInterface $clock): void
43+
{
44+
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
45+
}
46+
47+
public function now(): \DateTimeImmutable
48+
{
49+
$now = ($this->clock ?? self::$globalClock)->now();
50+
51+
return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
52+
}
53+
54+
public function sleep(float|int $seconds): void
55+
{
56+
$clock = $this->clock ?? self::$globalClock;
57+
58+
if ($clock instanceof ClockInterface) {
59+
$clock->sleep($seconds);
60+
} else {
61+
(new NativeClock())->sleep($seconds);
62+
}
63+
}
64+
65+
public function withTimeZone(\DateTimeZone|string $timezone): static
66+
{
67+
$clone = clone $this;
68+
$clone->timezone = \is_string($timezone) ? new \DateTimeZone($timezone) : $timezone;
69+
70+
return $clone;
71+
}
72+
}

MockClock.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
/**
1515
* A clock that always returns the same date, suitable for testing time-sensitive logic.
1616
*
17+
* Consider using ClockSensitiveTrait in your test cases instead of using this class directly.
18+
*
1719
* @author Nicolas Grekas <[email protected]>
1820
*/
1921
final class MockClock implements ClockInterface

Resources/now.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Clock;
13+
14+
/**
15+
* Returns the current time as a DateTimeImmutable.
16+
*
17+
* Note that you should prefer injecting a ClockInterface or using
18+
* ClockAwareTrait when possible instead of using this function.
19+
*/
20+
function now(): \DateTimeImmutable
21+
{
22+
return Clock::get()->now();
23+
}

Test/ClockSensitiveTrait.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Clock\Test;
13+
14+
use Psr\Clock\ClockInterface;
15+
use Symfony\Component\Clock\Clock;
16+
use Symfony\Component\Clock\MockClock;
17+
18+
use function Symfony\Component\Clock\now;
19+
20+
/**
21+
* Helps with mocking the time in your test cases.
22+
*
23+
* This trait provides one self::mockTime() method that freezes the time.
24+
* It restores the global clock after each test case.
25+
* self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'),
26+
* a DateTimeImmutable, or a boolean (to freeze/restore the global clock).
27+
*
28+
* @author Nicolas Grekas <[email protected]>
29+
*/
30+
trait ClockSensitiveTrait
31+
{
32+
public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface
33+
{
34+
Clock::set(match (true) {
35+
false === $when => self::saveClockBeforeTest(false),
36+
true === $when => new MockClock(),
37+
$when instanceof \DateTimeImmutable => new MockClock($when),
38+
default => new MockClock(now()->modify($when)),
39+
});
40+
41+
return Clock::get();
42+
}
43+
44+
/**
45+
* @before
46+
*
47+
* @internal
48+
*/
49+
protected static function saveClockBeforeTest(bool $save = true): ClockInterface
50+
{
51+
static $originalClock;
52+
53+
return $save ? $originalClock = Clock::get() : $originalClock;
54+
}
55+
56+
/**
57+
* @after
58+
*
59+
* @internal
60+
*/
61+
protected static function restoreClockAfterTest(): void
62+
{
63+
Clock::set(self::saveClockBeforeTest(false));
64+
}
65+
}

Tests/ClockTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Clock\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Psr\Clock\ClockInterface;
16+
use Symfony\Component\Clock\Clock;
17+
use Symfony\Component\Clock\MockClock;
18+
use Symfony\Component\Clock\NativeClock;
19+
use Symfony\Component\Clock\Test\ClockSensitiveTrait;
20+
21+
use function Symfony\Component\Clock\now;
22+
23+
class ClockTest extends TestCase
24+
{
25+
use ClockSensitiveTrait;
26+
27+
public function testMockClock()
28+
{
29+
$this->assertInstanceOf(NativeClock::class, Clock::get());
30+
31+
$clock = self::mockTime();
32+
$this->assertInstanceOf(MockClock::class, Clock::get());
33+
$this->assertSame(Clock::get(), $clock);
34+
}
35+
36+
public function testNativeClock()
37+
{
38+
$this->assertInstanceOf(\DateTimeImmutable::class, now());
39+
$this->assertInstanceOf(NativeClock::class, Clock::get());
40+
}
41+
42+
public function testMockClockDisable()
43+
{
44+
$this->assertInstanceOf(NativeClock::class, Clock::get());
45+
46+
$this->assertInstanceOf(MockClock::class, self::mockTime(true));
47+
$this->assertInstanceOf(NativeClock::class, self::mockTime(false));
48+
}
49+
50+
public function testMockClockFreeze()
51+
{
52+
self::mockTime(new \DateTimeImmutable('2021-12-19'));
53+
54+
$this->assertSame('2021-12-19', now()->format('Y-m-d'));
55+
56+
self::mockTime('+1 days');
57+
$this->assertSame('2021-12-20', now()->format('Y-m-d'));
58+
}
59+
60+
public function testPsrClock()
61+
{
62+
$psrClock = new class() implements ClockInterface {
63+
public function now(): \DateTimeImmutable
64+
{
65+
return new \DateTimeImmutable('@1234567');
66+
}
67+
};
68+
69+
Clock::set($psrClock);
70+
71+
$this->assertInstanceOf(Clock::class, Clock::get());
72+
73+
$this->assertSame(1234567, now()->getTimestamp());
74+
75+
$this->assertSame('UTC', Clock::get()->withTimeZone('UTC')->now()->getTimezone()->getName());
76+
$this->assertSame('Europe/Paris', Clock::get()->withTimeZone('Europe/Paris')->now()->getTimezone()->getName());
77+
78+
Clock::get()->sleep(0.1);
79+
80+
$this->assertSame(1234567, now()->getTimestamp());
81+
}
82+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"psr/clock": "^1.0"
2424
},
2525
"autoload": {
26+
"files": [ "Resources/now.php" ],
2627
"psr-4": { "Symfony\\Component\\Clock\\": "" },
2728
"exclude-from-classmap": [
2829
"/Tests/"

0 commit comments

Comments
 (0)