├── CHANGELOG.md ├── Clock.php ├── ClockAwareTrait.php ├── ClockInterface.php ├── DatePoint.php ├── LICENSE ├── MockClock.php ├── MonotonicClock.php ├── NativeClock.php ├── README.md ├── Resources └── now.php ├── Test └── ClockSensitiveTrait.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.1 5 | --- 6 | 7 | * Add `DatePoint::getMicrosecond()` and `DatePoint::setMicrosecond()` 8 | 9 | 6.4 10 | --- 11 | 12 | * Add `DatePoint`: an immutable DateTime implementation with stricter error handling and return types 13 | * Throw `DateMalformedStringException`/`DateInvalidTimeZoneException` when appropriate 14 | * Add `$modifier` argument to the `now()` helper 15 | 16 | 6.3 17 | --- 18 | 19 | * Add `ClockAwareTrait` to help write time-sensitive classes 20 | * Add `Clock` class and `now()` function 21 | 22 | 6.2 23 | --- 24 | 25 | * Add the component 26 | -------------------------------------------------------------------------------- /Clock.php: -------------------------------------------------------------------------------- 1 | 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
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(): DatePoint 48 | { 49 | $now = ($this->clock ?? self::get())->now(); 50 | 51 | if (!$now instanceof DatePoint) { 52 | $now = DatePoint::createFromInterface($now); 53 | } 54 | 55 | return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now; 56 | } 57 | 58 | public function sleep(float|int $seconds): void 59 | { 60 | $clock = $this->clock ?? self::get(); 61 | 62 | if ($clock instanceof ClockInterface) { 63 | $clock->sleep($seconds); 64 | } else { 65 | (new NativeClock())->sleep($seconds); 66 | } 67 | } 68 | 69 | /** 70 | * @throws \DateInvalidTimeZoneException When $timezone is invalid 71 | */ 72 | public function withTimeZone(\DateTimeZone|string $timezone): static 73 | { 74 | if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) { 75 | $timezone = new \DateTimeZone($timezone); 76 | } elseif (\is_string($timezone)) { 77 | try { 78 | $timezone = new \DateTimeZone($timezone); 79 | } catch (\Exception $e) { 80 | throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e); 81 | } 82 | } 83 | 84 | $clone = clone $this; 85 | $clone->timezone = $timezone; 86 | 87 | return $clone; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ClockAwareTrait.php: -------------------------------------------------------------------------------- 1 | 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; 15 | use Symfony\Contracts\Service\Attribute\Required; 16 | 17 | /** 18 | * A trait to help write time-sensitive classes. 19 | * 20 | * @author Nicolas Grekas
21 | */ 22 | trait ClockAwareTrait 23 | { 24 | private readonly ClockInterface $clock; 25 | 26 | #[Required] 27 | public function setClock(ClockInterface $clock): void 28 | { 29 | $this->clock = $clock; 30 | } 31 | 32 | protected function now(): DatePoint 33 | { 34 | $now = ($this->clock ??= new Clock())->now(); 35 | 36 | return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ClockInterface.php: -------------------------------------------------------------------------------- 1 | 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 | * @author Nicolas Grekas
18 | */ 19 | interface ClockInterface extends PsrClockInterface 20 | { 21 | public function sleep(float|int $seconds): void; 22 | 23 | public function withTimeZone(\DateTimeZone|string $timezone): static; 24 | } 25 | -------------------------------------------------------------------------------- /DatePoint.php: -------------------------------------------------------------------------------- 1 | 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 | * An immmutable DateTime with stricter error handling and return types than the native one. 16 | * 17 | * @author Nicolas Grekas
18 | */ 19 | final class DatePoint extends \DateTimeImmutable 20 | { 21 | /** 22 | * @throws \DateMalformedStringException When $datetime is invalid 23 | */ 24 | public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null, ?parent $reference = null) 25 | { 26 | $now = $reference ?? Clock::get()->now(); 27 | 28 | if ('now' !== $datetime) { 29 | if (!$now instanceof static) { 30 | $now = static::createFromInterface($now); 31 | } 32 | 33 | if (\PHP_VERSION_ID < 80300) { 34 | try { 35 | $builtInDate = new parent($datetime, $timezone ?? $now->getTimezone()); 36 | $timezone = $builtInDate->getTimezone(); 37 | } catch (\Exception $e) { 38 | throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e); 39 | } 40 | } else { 41 | $builtInDate = new parent($datetime, $timezone ?? $now->getTimezone()); 42 | $timezone = $builtInDate->getTimezone(); 43 | } 44 | 45 | $now = $now->setTimezone($timezone)->modify($datetime); 46 | 47 | if ('00:00:00.000000' === $builtInDate->format('H:i:s.u')) { 48 | $now = $now->setTime(0, 0); 49 | } 50 | } elseif (null !== $timezone) { 51 | $now = $now->setTimezone($timezone); 52 | } 53 | 54 | $this->__unserialize((array) $now); 55 | } 56 | 57 | /** 58 | * @throws \DateMalformedStringException When $format or $datetime are invalid 59 | */ 60 | public static function createFromFormat(string $format, string $datetime, ?\DateTimeZone $timezone = null): static 61 | { 62 | return parent::createFromFormat($format, $datetime, $timezone) ?: throw new \DateMalformedStringException(static::getLastErrors()['errors'][0] ?? 'Invalid date string or format.'); 63 | } 64 | 65 | public static function createFromInterface(\DateTimeInterface $object): static 66 | { 67 | return parent::createFromInterface($object); 68 | } 69 | 70 | public static function createFromMutable(\DateTime $object): static 71 | { 72 | return parent::createFromMutable($object); 73 | } 74 | 75 | public static function createFromTimestamp(int|float $timestamp): static 76 | { 77 | if (\PHP_VERSION_ID >= 80400) { 78 | return parent::createFromTimestamp($timestamp); 79 | } 80 | 81 | if (\is_int($timestamp) || !$ms = (int) $timestamp - $timestamp) { 82 | return static::createFromFormat('U', (string) $timestamp); 83 | } 84 | 85 | if (!is_finite($timestamp) || \PHP_INT_MAX + 1.0 <= $timestamp || \PHP_INT_MIN > $timestamp) { 86 | throw new \DateRangeError(\sprintf('DateTimeImmutable::createFromTimestamp(): Argument #1 ($timestamp) must be a finite number between %s and %s.999999, %s given', \PHP_INT_MIN, \PHP_INT_MAX, $timestamp)); 87 | } 88 | 89 | if ($timestamp < 0) { 90 | $timestamp = (int) $timestamp - 2.0 + $ms; 91 | } 92 | 93 | return static::createFromFormat('U.u', \sprintf('%.6F', $timestamp)); 94 | } 95 | 96 | public function add(\DateInterval $interval): static 97 | { 98 | return parent::add($interval); 99 | } 100 | 101 | public function sub(\DateInterval $interval): static 102 | { 103 | return parent::sub($interval); 104 | } 105 | 106 | /** 107 | * @throws \DateMalformedStringException When $modifier is invalid 108 | */ 109 | public function modify(string $modifier): static 110 | { 111 | if (\PHP_VERSION_ID < 80300) { 112 | return @parent::modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? \sprintf('Invalid modifier: "%s".', $modifier)); 113 | } 114 | 115 | return parent::modify($modifier); 116 | } 117 | 118 | public function setTimestamp(int $value): static 119 | { 120 | return parent::setTimestamp($value); 121 | } 122 | 123 | public function setDate(int $year, int $month, int $day): static 124 | { 125 | return parent::setDate($year, $month, $day); 126 | } 127 | 128 | public function setISODate(int $year, int $week, int $day = 1): static 129 | { 130 | return parent::setISODate($year, $week, $day); 131 | } 132 | 133 | public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static 134 | { 135 | return parent::setTime($hour, $minute, $second, $microsecond); 136 | } 137 | 138 | public function setTimezone(\DateTimeZone $timezone): static 139 | { 140 | return parent::setTimezone($timezone); 141 | } 142 | 143 | public function getTimezone(): \DateTimeZone 144 | { 145 | return parent::getTimezone() ?: throw new \DateInvalidTimeZoneException('The DatePoint object has no timezone.'); 146 | } 147 | 148 | public function setMicrosecond(int $microsecond): static 149 | { 150 | if ($microsecond < 0 || $microsecond > 999999) { 151 | throw new \DateRangeError('DatePoint::setMicrosecond(): Argument #1 ($microsecond) must be between 0 and 999999, '.$microsecond.' given'); 152 | } 153 | 154 | if (\PHP_VERSION_ID < 80400) { 155 | return $this->setTime(...explode('.', $this->format('H.i.s.'.$microsecond))); 156 | } 157 | 158 | return parent::setMicrosecond($microsecond); 159 | } 160 | 161 | public function getMicrosecond(): int 162 | { 163 | if (\PHP_VERSION_ID >= 80400) { 164 | return parent::getMicrosecond(); 165 | } 166 | 167 | return $this->format('u'); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MockClock.php: -------------------------------------------------------------------------------- 1 | 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 | * A clock that always returns the same date, suitable for testing time-sensitive logic. 16 | * 17 | * Consider using ClockSensitiveTrait in your test cases instead of using this class directly. 18 | * 19 | * @author Nicolas Grekas
20 | */ 21 | final class MockClock implements ClockInterface 22 | { 23 | private DatePoint $now; 24 | 25 | /** 26 | * @throws \DateMalformedStringException When $now is invalid 27 | * @throws \DateInvalidTimeZoneException When $timezone is invalid 28 | */ 29 | public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string|null $timezone = null) 30 | { 31 | if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) { 32 | $timezone = new \DateTimeZone($timezone); 33 | } elseif (\is_string($timezone)) { 34 | try { 35 | $timezone = new \DateTimeZone($timezone); 36 | } catch (\Exception $e) { 37 | throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e); 38 | } 39 | } 40 | 41 | if (\is_string($now)) { 42 | $now = new DatePoint($now, $timezone ?? new \DateTimeZone('UTC')); 43 | } elseif (!$now instanceof DatePoint) { 44 | $now = DatePoint::createFromInterface($now); 45 | } 46 | 47 | $this->now = null !== $timezone ? $now->setTimezone($timezone) : $now; 48 | } 49 | 50 | public function now(): DatePoint 51 | { 52 | return clone $this->now; 53 | } 54 | 55 | public function sleep(float|int $seconds): void 56 | { 57 | $now = (float) $this->now->format('Uu') + $seconds * 1e6; 58 | $now = substr_replace(\sprintf('@%07.0F', $now), '.', -6, 0); 59 | $timezone = $this->now->getTimezone(); 60 | 61 | $this->now = DatePoint::createFromInterface(new \DateTimeImmutable($now, $timezone))->setTimezone($timezone); 62 | } 63 | 64 | /** 65 | * @throws \DateMalformedStringException When $modifier is invalid 66 | */ 67 | public function modify(string $modifier): void 68 | { 69 | if (\PHP_VERSION_ID < 80300) { 70 | $this->now = @$this->now->modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? \sprintf('Invalid modifier: "%s". Could not modify MockClock.', $modifier)); 71 | 72 | return; 73 | } 74 | 75 | $this->now = $this->now->modify($modifier); 76 | } 77 | 78 | /** 79 | * @throws \DateInvalidTimeZoneException When the timezone name is invalid 80 | */ 81 | public function withTimeZone(\DateTimeZone|string $timezone): static 82 | { 83 | if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) { 84 | $timezone = new \DateTimeZone($timezone); 85 | } elseif (\is_string($timezone)) { 86 | try { 87 | $timezone = new \DateTimeZone($timezone); 88 | } catch (\Exception $e) { 89 | throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e); 90 | } 91 | } 92 | 93 | $clone = clone $this; 94 | $clone->now = $clone->now->setTimezone($timezone); 95 | 96 | return $clone; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MonotonicClock.php: -------------------------------------------------------------------------------- 1 | 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 | * A monotonic clock suitable for performance profiling. 16 | * 17 | * @author Nicolas Grekas
18 | */ 19 | final class MonotonicClock implements ClockInterface 20 | { 21 | private int $sOffset; 22 | private int $usOffset; 23 | private \DateTimeZone $timezone; 24 | 25 | /** 26 | * @throws \DateInvalidTimeZoneException When $timezone is invalid 27 | */ 28 | public function __construct(\DateTimeZone|string|null $timezone = null) 29 | { 30 | if (false === $offset = hrtime()) { 31 | throw new \RuntimeException('hrtime() returned false: the runtime environment does not provide access to a monotonic timer.'); 32 | } 33 | 34 | $time = explode(' ', microtime(), 2); 35 | $this->sOffset = $time[1] - $offset[0]; 36 | $this->usOffset = (int) ($time[0] * 1000000) - (int) ($offset[1] / 1000); 37 | 38 | $this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone; 39 | } 40 | 41 | public function now(): DatePoint 42 | { 43 | [$s, $us] = hrtime(); 44 | 45 | if (1000000 <= $us = (int) ($us / 1000) + $this->usOffset) { 46 | ++$s; 47 | $us -= 1000000; 48 | } elseif (0 > $us) { 49 | --$s; 50 | $us += 1000000; 51 | } 52 | 53 | if (6 !== \strlen($now = (string) $us)) { 54 | $now = str_pad($now, 6, '0', \STR_PAD_LEFT); 55 | } 56 | 57 | $now = '@'.($s + $this->sOffset).'.'.$now; 58 | 59 | return DatePoint::createFromInterface(new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone); 60 | } 61 | 62 | public function sleep(float|int $seconds): void 63 | { 64 | if (0 < $s = (int) $seconds) { 65 | sleep($s); 66 | } 67 | 68 | if (0 < $us = $seconds - $s) { 69 | usleep((int) ($us * 1E6)); 70 | } 71 | } 72 | 73 | /** 74 | * @throws \DateInvalidTimeZoneException When $timezone is invalid 75 | */ 76 | public function withTimeZone(\DateTimeZone|string $timezone): static 77 | { 78 | if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) { 79 | $timezone = new \DateTimeZone($timezone); 80 | } elseif (\is_string($timezone)) { 81 | try { 82 | $timezone = new \DateTimeZone($timezone); 83 | } catch (\Exception $e) { 84 | throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e); 85 | } 86 | } 87 | 88 | $clone = clone $this; 89 | $clone->timezone = $timezone; 90 | 91 | return $clone; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /NativeClock.php: -------------------------------------------------------------------------------- 1 | 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 | * A clock that relies the system time. 16 | * 17 | * @author Nicolas Grekas
18 | */ 19 | final class NativeClock implements ClockInterface 20 | { 21 | private \DateTimeZone $timezone; 22 | 23 | /** 24 | * @throws \DateInvalidTimeZoneException When $timezone is invalid 25 | */ 26 | public function __construct(\DateTimeZone|string|null $timezone = null) 27 | { 28 | $this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone; 29 | } 30 | 31 | public function now(): DatePoint 32 | { 33 | return DatePoint::createFromInterface(new \DateTimeImmutable('now', $this->timezone)); 34 | } 35 | 36 | public function sleep(float|int $seconds): void 37 | { 38 | if (0 < $s = (int) $seconds) { 39 | sleep($s); 40 | } 41 | 42 | if (0 < $us = $seconds - $s) { 43 | usleep((int) ($us * 1E6)); 44 | } 45 | } 46 | 47 | /** 48 | * @throws \DateInvalidTimeZoneException When $timezone is invalid 49 | */ 50 | public function withTimeZone(\DateTimeZone|string $timezone): static 51 | { 52 | if (\PHP_VERSION_ID >= 80300 && \is_string($timezone)) { 53 | $timezone = new \DateTimeZone($timezone); 54 | } elseif (\is_string($timezone)) { 55 | try { 56 | $timezone = new \DateTimeZone($timezone); 57 | } catch (\Exception $e) { 58 | throw new \DateInvalidTimeZoneException($e->getMessage(), $e->getCode(), $e); 59 | } 60 | } 61 | 62 | $clone = clone $this; 63 | $clone->timezone = $timezone; 64 | 65 | return $clone; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Clock Component 2 | =============== 3 | 4 | Symfony Clock decouples applications from the system clock. 5 | 6 | Getting Started 7 | --------------- 8 | 9 | ```bash 10 | composer require symfony/clock 11 | ``` 12 | 13 | ```php 14 | use Symfony\Component\Clock\NativeClock; 15 | use Symfony\Component\Clock\ClockInterface; 16 | 17 | class MyClockSensitiveClass 18 | { 19 | public function __construct( 20 | private ClockInterface $clock, 21 | ) { 22 | // Only if you need to force a timezone: 23 | //$this->clock = $clock->withTimeZone('UTC'); 24 | } 25 | 26 | public function doSomething() 27 | { 28 | $now = $this->clock->now(); 29 | // [...] do something with $now, which is a \DateTimeImmutable object 30 | 31 | $this->clock->sleep(2.5); // Pause execution for 2.5 seconds 32 | } 33 | } 34 | 35 | $clock = new NativeClock(); 36 | $service = new MyClockSensitiveClass($clock); 37 | $service->doSomething(); 38 | ``` 39 | 40 | Resources 41 | --------- 42 | 43 | * [Documentation](https://symfony.com/doc/current/components/clock.html) 44 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 45 | * [Report issues](https://github.com/symfony/symfony/issues) and 46 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 47 | in the [main Symfony repository](https://github.com/symfony/symfony) 48 | -------------------------------------------------------------------------------- /Resources/now.php: -------------------------------------------------------------------------------- 1 | 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 | if (!\function_exists(now::class)) { 15 | /** 16 | * @throws \DateMalformedStringException When the modifier is invalid 17 | */ 18 | function now(string $modifier = 'now'): DatePoint 19 | { 20 | if ('now' !== $modifier) { 21 | return new DatePoint($modifier); 22 | } 23 | 24 | $now = Clock::get()->now(); 25 | 26 | return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Test/ClockSensitiveTrait.php: -------------------------------------------------------------------------------- 1 | 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 PHPUnit\Framework\Attributes\After; 15 | use PHPUnit\Framework\Attributes\Before; 16 | use PHPUnit\Framework\Attributes\BeforeClass; 17 | use Symfony\Component\Clock\Clock; 18 | use Symfony\Component\Clock\ClockInterface; 19 | use Symfony\Component\Clock\MockClock; 20 | 21 | use function Symfony\Component\Clock\now; 22 | 23 | /** 24 | * Helps with mocking the time in your test cases. 25 | * 26 | * This trait provides one self::mockTime() method that freezes the time. 27 | * It restores the global clock after each test case. 28 | * self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'), 29 | * a DateTimeImmutable, or a boolean (to freeze/restore the global clock). 30 | * 31 | * @author Nicolas Grekas
32 | */ 33 | trait ClockSensitiveTrait 34 | { 35 | public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface 36 | { 37 | Clock::set(match (true) { 38 | false === $when => self::saveClockBeforeTest(false), 39 | true === $when => new MockClock(), 40 | $when instanceof \DateTimeImmutable => new MockClock($when), 41 | default => new MockClock(now($when)), 42 | }); 43 | 44 | return Clock::get(); 45 | } 46 | 47 | /** 48 | * @beforeClass 49 | * 50 | * @before 51 | * 52 | * @internal 53 | */ 54 | #[Before] 55 | #[BeforeClass] 56 | public static function saveClockBeforeTest(bool $save = true): ClockInterface 57 | { 58 | static $originalClock; 59 | 60 | if ($save && $originalClock) { 61 | self::restoreClockAfterTest(); 62 | } 63 | 64 | return $save ? $originalClock = Clock::get() : $originalClock; 65 | } 66 | 67 | /** 68 | * @after 69 | * 70 | * @internal 71 | */ 72 | #[After] 73 | protected static function restoreClockAfterTest(): void 74 | { 75 | Clock::set(self::saveClockBeforeTest(false)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/clock", 3 | "type": "library", 4 | "description": "Decouples applications from the system clock", 5 | "keywords": ["clock", "time", "psr20"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nicolas Grekas", 11 | "email": "p@tchwork.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "provide": { 19 | "psr/clock-implementation": "1.0" 20 | }, 21 | "require": { 22 | "php": ">=8.2", 23 | "psr/clock": "^1.0", 24 | "symfony/polyfill-php83": "^1.28" 25 | }, 26 | "autoload": { 27 | "files": [ "Resources/now.php" ], 28 | "psr-4": { "Symfony\\Component\\Clock\\": "" }, 29 | "exclude-from-classmap": [ 30 | "/Tests/" 31 | ] 32 | }, 33 | "minimum-stability": "dev" 34 | } 35 | --------------------------------------------------------------------------------