Skip to content

Commit

Permalink
[Clock] Add Clock class and now() function
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Dec 19, 2022
1 parent bd6219d commit 43d3143
Show file tree
Hide file tree
Showing 9 changed files with 248 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;
}
}
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();
}
56 changes: 56 additions & 0 deletions src/Symfony/Component/Clock/Test/ClockSensitiveTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?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;

trait ClockSensitiveTrait
{
public static function mockTime(string|\DateTimeImmutable|ClockInterface|bool $when = true): ClockInterface
{
Clock::set(match (true) {
false === $when => self::saveClockBeforeTest(false),
true === $when => new MockClock(),
$when instanceof ClockInterface => $when,
$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));
}
}
91 changes: 91 additions & 0 deletions src/Symfony/Component/Clock/Tests/ClockTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?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 testMockClockWithClock()
{
$mockClock = new MockClock();

self::mockTime($mockClock);

$this->assertSame($mockClock, Clock::get());
}

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 43d3143

Please sign in to comment.