Skip to content

Commit

Permalink
feature #48642 [Clock] Add Clock class and now() function (nicola…
Browse files Browse the repository at this point in the history
…s-grekas)

This PR was merged into the 6.3 branch.

Discussion
----------

[Clock] Add `Clock` class and `now()` function

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #48564
| License       | MIT
| Doc PR        | -

See discussion on #48564.

This PR adds 2 static methods and one function:
```php

namespace Symfony\Component\Clock;

Clock::get(): ClockInterface;
Clock::set(PsrClockInterface $clock): void;

now(): \DateTimeImmutable;
```

It also wires this global clock as a service so that injecting the `ClockInterface` or using `now` / `Clock::get()` returns the same time.

Last but not least, this PR also provides a `ClockSensitiveTrait` to help write test cases that rely on the clock. This trait provides one `self::mockTime()` method and it restores the global clock after each test case.

`mockTime()` accepts either a string (eg `'+1 days'` or `'2022-12-22'`), a DTI instance, or a boolean (to freeze/restore the global clock).

Commits
-------

aaad65c [Clock] Add `Clock` class and `now()` function
  • Loading branch information
nicolas-grekas committed Dec 22, 2022
2 parents a826eb3 + aaad65c commit 5bfb260
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 2 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
},
"autoload-dev": {
"files": [
"src/Symfony/Component/Clock/Resources/now.php",
"src/Symfony/Component/VarDumper/Resources/functions/dump.php"
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer;
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;
use Symfony\Component\Config\ResourceCheckerConfigCacheFactory;
Expand Down Expand Up @@ -229,7 +229,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
->args([service(KernelInterface::class), service('logger')->nullOnInvalid()])
->tag('kernel.cache_warmer')

->set('clock', NativeClock::class)
->set('clock', Clock::class)
->alias(ClockInterface::class, 'clock')
->alias(PsrClockInterface::class, 'clock')

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"phpdocumentor/type-resolver": "<1.4.0",
"phpunit/phpunit": "<5.4.3",
"symfony/asset": "<5.4",
"symfony/clock": "<6.3",
"symfony/console": "<5.4",
"symfony/dotenv": "<5.4",
"symfony/dom-crawler": "<5.4",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Clock/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

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

6.2
---
Expand Down
72 changes: 72 additions & 0 deletions src/Symfony/Component/Clock/Clock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Clock;

use Psr\Clock\ClockInterface as PsrClockInterface;

/**
* A global clock.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Clock implements ClockInterface
{
private static ClockInterface $globalClock;

public function __construct(
private readonly ?PsrClockInterface $clock = null,
private ?\DateTimeZone $timezone = null,
) {
}

/**
* Returns the current global clock.
*
* Note that you should prefer injecting a ClockInterface or using
* ClockAwareTrait when possible instead of using this method.
*/
public static function get(): ClockInterface
{
return self::$globalClock ??= new NativeClock();
}

public static function set(PsrClockInterface $clock): void
{
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
}

public function now(): \DateTimeImmutable
{
$now = ($this->clock ?? self::$globalClock)->now();

return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
}

public function sleep(float|int $seconds): void
{
$clock = $this->clock ?? self::$globalClock;

if ($clock instanceof ClockInterface) {
$clock->sleep($seconds);
} else {
(new NativeClock())->sleep($seconds);
}
}

public function withTimeZone(\DateTimeZone|string $timezone): static
{
$clone = clone $this;
$clone->timezone = \is_string($timezone) ? new \DateTimeZone($timezone) : $timezone;

return $clone;
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Component/Clock/MockClock.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
/**
* A clock that always returns the same date, suitable for testing time-sensitive logic.
*
* Consider using ClockSensitiveTrait in your test cases instead of using this class directly.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class MockClock implements ClockInterface
Expand Down
23 changes: 23 additions & 0 deletions src/Symfony/Component/Clock/Resources/now.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Clock;

/**
* Returns the current time as a DateTimeImmutable.
*
* Note that you should prefer injecting a ClockInterface or using
* ClockAwareTrait when possible instead of using this function.
*/
function now(): \DateTimeImmutable
{
return Clock::get()->now();
}
65 changes: 65 additions & 0 deletions src/Symfony/Component/Clock/Test/ClockSensitiveTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Clock\Test;

use Psr\Clock\ClockInterface;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\MockClock;

use function Symfony\Component\Clock\now;

/**
* Helps with mocking the time in your test cases.
*
* This trait provides one self::mockTime() method that freezes the time.
* It restores the global clock after each test case.
* self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'),
* a DateTimeImmutable, or a boolean (to freeze/restore the global clock).
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ClockSensitiveTrait
{
public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface
{
Clock::set(match (true) {
false === $when => self::saveClockBeforeTest(false),
true === $when => new MockClock(),
$when instanceof \DateTimeImmutable => new MockClock($when),
default => new MockClock(now()->modify($when)),
});

return Clock::get();
}

/**
* @before
*
* @internal
*/
protected static function saveClockBeforeTest(bool $save = true): ClockInterface
{
static $originalClock;

return $save ? $originalClock = Clock::get() : $originalClock;
}

/**
* @after
*
* @internal
*/
protected static function restoreClockAfterTest(): void
{
Clock::set(self::saveClockBeforeTest(false));
}
}
82 changes: 82 additions & 0 deletions src/Symfony/Component/Clock/Tests/ClockTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Clock\Tests;

use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
use Symfony\Component\Clock\Clock;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Clock\NativeClock;
use Symfony\Component\Clock\Test\ClockSensitiveTrait;

use function Symfony\Component\Clock\now;

class ClockTest extends TestCase
{
use ClockSensitiveTrait;

public function testMockClock()
{
$this->assertInstanceOf(NativeClock::class, Clock::get());

$clock = self::mockTime();
$this->assertInstanceOf(MockClock::class, Clock::get());
$this->assertSame(Clock::get(), $clock);
}

public function testNativeClock()
{
$this->assertInstanceOf(\DateTimeImmutable::class, now());
$this->assertInstanceOf(NativeClock::class, Clock::get());
}

public function testMockClockDisable()
{
$this->assertInstanceOf(NativeClock::class, Clock::get());

$this->assertInstanceOf(MockClock::class, self::mockTime(true));
$this->assertInstanceOf(NativeClock::class, self::mockTime(false));
}

public function testMockClockFreeze()
{
self::mockTime(new \DateTimeImmutable('2021-12-19'));

$this->assertSame('2021-12-19', now()->format('Y-m-d'));

self::mockTime('+1 days');
$this->assertSame('2021-12-20', now()->format('Y-m-d'));
}

public function testPsrClock()
{
$psrClock = new class() implements ClockInterface {
public function now(): \DateTimeImmutable
{
return new \DateTimeImmutable('@1234567');
}
};

Clock::set($psrClock);

$this->assertInstanceOf(Clock::class, Clock::get());

$this->assertSame(1234567, now()->getTimestamp());

$this->assertSame('UTC', Clock::get()->withTimeZone('UTC')->now()->getTimezone()->getName());
$this->assertSame('Europe/Paris', Clock::get()->withTimeZone('Europe/Paris')->now()->getTimezone()->getName());

Clock::get()->sleep(0.1);

$this->assertSame(1234567, now()->getTimestamp());
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Clock/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"psr/clock": "^1.0"
},
"autoload": {
"files": [ "Resources/now.php" ],
"psr-4": { "Symfony\\Component\\Clock\\": "" },
"exclude-from-classmap": [
"/Tests/"
Expand Down

0 comments on commit 5bfb260

Please sign in to comment.