Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Clock] Add Clock class and now() function #48642

Merged
merged 1 commit into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved
*
* @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.
Comment on lines +17 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

*/
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
nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved
{
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)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this be confusing if now() is not a native clock currently ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not a native clock, it's usually a mock clock. This behavior is desired to me, to do eg:

self::mockTime(); // freeze
//...
self::mockTime('+1 days'); // frozen time + 1 day

});

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());
nicolas-grekas marked this conversation as resolved.
Show resolved Hide resolved

$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