8000 feature #46715 [Clock] A new component to decouple applications from … · symfony/symfony@f94700c · GitHub
[go: up one dir, main page]

Skip to content

Commit f94700c

Browse files
committed
feature #46715 [Clock] A new component to decouple applications from the system clock (nicolas-grekas)
This PR was squashed before being merged into the 6.2 branch. Discussion ---------- [Clock] A new component to decouple applications from the system clock | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - After watching @afilina's talk at SymfonyWorld Online last week + listening to her using a "clock" service to improve testability of time-sensitive logics, I decided to propose this new "Clock" component. This also relates to the ongoing efforts to standardize a `ClockInterface` in [PSR-20](https://github.com/php-fig/clock). This PR provides a `ClockInterface`, with 3 methods: - `now(): \DateTimeImmutable` is designed to be compatible with the envisioned PSR-20; - `sleep(float|int $seconds): void` advances the clock by the provided number of seconds; - `withTimeZone(): static` changes the time zone returned by `now()`. The `sleep()` methods takes inspiration from `ClockMock` in the PhpUnitBridge, where this proved useful to improve testability. Ideally, we could use this component everywhere measuring the current time is needed and stop relying on `ClockMock`. This PR provides 3 clock implementations: - `NativeClock` which relies on the system clock; - `MockClock` which allows mocking the time; - `MonotonicClock` which relies on `hrtime()` to provide a monotonic clock. If this gets accepted, I'll follow up with a PR to add clock services to FrameworkBundle and we'll then be able to see where we could use such clock services in other components. I hope PSR-20 will be stabilized by Symfony 6.2, but this is not required for this PR to be useful. Commits ------- 9db08e4 [Clock] A new component to decouple applications from the system clock
2 parents 91d0168 + 9db08e4 commit f94700c

17 files changed

+590
-3
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"symfony/asset": "self.version",
5858
"symfony/browser-kit": "self.version",
5959
"symfony/cache": "self.version",
60+
"symfony/clock": "self.version",
6061
"symfony/config": "self.version",
6162
"symfony/console": "self.version",
6263
"symfony/css-selector": "self.version",

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
use PHPStan\PhpDocParser\Parser\PhpDocParser;
2121
use Psr\Cache\CacheItemPoolInterface;
2222
use Psr\Container\ContainerInterface as PsrContainerInterface;
23-
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
2423
use Psr\Http\Client\ClientInterface;
2524
use Psr\Log\LoggerAwareInterface;
2625
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
@@ -40,6 +39,7 @@
4039
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
4140
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
4241
use Symfony\Component\Cache\ResettableInterface;
42+
use Symfony\Component\Clock\ClockInterface;
4343
use Symfony\Component\Config\Definition\ConfigurationInterface;
4444
use Symfony\Component\Config\FileLocator;
4545
use Symfony\Component\Config\Loader\LoaderInterface;
@@ -277,8 +277,9 @@ public function load(array $configs, ContainerBuilder $container)
277277
$loader->load('fragment_renderer.php');
278278
$loader->load('error_renderer.php');
279279

280-
if (ContainerBuilder::willBeAvailable('psr/event-dispatcher', PsrEventDispatcherInterface::class, ['symfony/framework-bundle'])) {
281-
$container->setAlias(PsrEventDispatcherInterface::class, 'event_dispatcher');
280+
if (!ContainerBuilder::willBeAvailable('symfony/clock', ClockInterface::class, ['symfony/framework-bundle'])) {
281+
$container->removeDefinition('clock');
282+
$container->removeAlias(ClockInterface::class);
282283
}
283284

284285
$container->registerAliasForArgument('parameter_bag', PsrContainerInterface::class);

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14+
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
1415
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer;
1516
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
17+
use Symfony\Component\Clock\ClockInterface;
18+
use Symfony\Component\Clock\NativeClock;
1619
use Symfony\Component\Config\Loader\LoaderInterface;
1720
use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;
1821
use Symfony\Component\Config\ResourceCheckerConfigCacheFactory;
@@ -77,6 +80,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
7780
->tag('event_dispatcher.dispatcher', ['name' => 'event_dispatcher'])
7881
->alias(EventDispatcherInterfaceComponentAlias::class, 'event_dispatcher')
7982
->alias(EventDispatcherInterface::class, 'event_dispatcher')
83+
->alias(PsrEventDispatcherInterface::class, 'event_dispatcher')
8084

8185
->set('http_kernel', HttpKernel::class)
8286
->public()
@@ -224,6 +228,9 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
224228
->args([service(KernelInterface::class), service('logger')->nullOnInvalid()])
225229
->tag('kernel.cache_warmer')
226230

231+
->set('clock', NativeClock::class)
232+
->alias(ClockInterface::class, 'clock')
233+
227234
// register as abstract and excluded, aka not-autowirable types
228235
->set(LoaderInterface::class)->abstract()->tag('container.excluded')
229236
->set(Request::class)->abstract()->tag('container.excluded')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
6.2
5+
---
6+
7+
* Add the component
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
* @author Nicolas Grekas <p@tchwork.com>
16+
*/
17+
interface ClockInterface
18+
{
19+
public function now(): \DateTimeImmutable;
20+
21+
public function sleep(float|int $seconds): void;
22+
23+
public function withTimeZone(\DateTimeZone|string $timezone): static;
24+
}

src/Symfony/Component/Clock/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2022 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
< 57AE code>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.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
final class MockClock implements ClockInterface
20+
{
21+
private \DateTimeImmutable $now;
22+
23+
public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string $timezone = null)
24+
{
25+
if (\is_string($timezone)) {
26+
$timezone = new \DateTimeZone($timezone);
27+
}
28+
29+
if (\is_string($now)) {
30+
$now = new \DateTimeImmutable($now, $timezone ?? new \DateTimeZone('UTC'));
31+
}
32+
33+
$this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
34+
}
35+
36+
public function now(): \DateTimeImmutable
37+
{
38+
return clone $this->now;
39+
}
40+
41+
public function sleep(float|int $seconds): void
42+
{
43+
$now = explode('.', $this->now->format('U.u'));
44+
45+
if (0 < $s = (int) $seconds) {
46+
$now[0] += $s;
47+
}
48+
49+
if (0 < ($us = $seconds - $s) && 1E6 <= $now[1] += $us * 1E6) {
50+
++$now[0];
51+
$now[1] -= 1E6;
52+
}
53+
54+
$datetime = '@'.$now[0].'.'.str_pad($now[1], 6, '0', \STR_PAD_LEFT);
55+
$timezone = $this->now->getTimezone();
56+
57+
$this->now = (new \DateTimeImmutable($datetime, $timezone))->setTimezone($timezone);
58+
}
59+
60+
public function withTimeZone(\DateTimeZone|string $timezone): static
61+
{
62+
$clone = clone $this;
63+
$clone->now = $clone->now->setTimezone(\is_string($timezone) ? new \DateTimeZone($timezone) : $timezone);
64+
65+
return $clone;
66+
}
67+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <p@tchwork.com>
18+
*/
19+
final class MonotonicClock implements ClockInterface
20+
{
21+
private int $sOffset;
22+
private int $usOffset;
23+
private \DateTimeZone $timezone;
24+
25+
public function __construct(\DateTimeZone|string $timezone = null)
26+
{
27+
if (false === $offset = hrtime()) {
28+
throw new \RuntimeException('hrtime() returned false: the runtime environment does not provide access to a monotonic timer.');
29+
}
30+
31+
$time = gettimeofday();
32+
$this->sOffset = $time['sec'] - $offset[0];
33+
$this->usOffset = $time['usec'] - (int) ($offset[1] / 1000);
34+
35+
if (\is_string($timezone ??= date_default_timezone_get())) {
36+
$this->timezone = new \DateTimeZone($timezone);
37+
} else {
38+
$this->timezone = $timezone;
39+
}
40+
}
41+
42+
public function now(): \DateTimeImmutable
43+
{
44+
[$s, $us] = hrtime();
45+
46+
if (1000000 <= $us = (int) ($us / 1000) + $this->usOffset) {
47+
++$s;
48+
$us -= 1000000;
49+
} elseif (0 > $us) {
50+
--$s;
51+
$us += 1000000;
52+
}
53+
54+
if (6 !== \strlen($now = (string) $us)) {
55+
$now = str_pad($now, 6, '0', \STR_PAD_LEFT);
56+
}
57+
58+
$now = '@'.($s + $this->sOffset).'.'.$now;
59+
60+
return (new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
61+
}
62+
63+
public function sleep(float|int $seconds): void
64+
{
65+
if (0 < $s = (int) $seconds) {
66+
sleep($s);
67+
}
68+
69+
if (0 < $us = $seconds - $s) {
70+
usleep($us * 1E6);
71+
}
72+
}
73+
74+
public function withTimeZone(\DateTimeZone|string $timezone): static
75+
{
76+
$clone = clone $this;
77+
$clone->timezone = \is_string($timezone) ? new \DateTimeZone($timezone) : $timezone;
78+
79+
return $clone;
80+
}
81+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <p@tchwork.com>
18+
*/
19+
final class NativeClock implements ClockInterface
20+
{
21+
private \DateTimeZone $timezone;
22+
23+
public function __construct(\DateTimeZone|string $timezone = null)
24+
{
25+
if (\is_string($timezone ??= date_default_timezone_get())) {
26+
$this->timezone = new \DateTimeZone($timezone);
27+
} else {
28+
$this->timezone = $timezone;
29+
}
30+
}
31+
32+
public function now(): \DateTimeImmutable
33+
{
34+
return new \DateTimeImmutable('now', $this->timezone);
35+
}
36+
37+
public function sleep(float|int $seconds): void
38+
{
39+
if (0 < $s = (int) $seconds) {
40+
sleep($s);
41+
}
42+
43+
if (0 < $us = $seconds - $s) {
44+
usleep($us * 1E6);
45+
}
46+
}
47+
48+
public function withTimeZone(\DateTimeZone|string $timezone): static
49+
{
50+
$clone = clone $this;
51+
$clone->timezone = \is_string($timezone) ? new \DateTimeZone($timezone) : $timezone;
52+
53+
return $clone;
54+
}
55+
}

src/Symfony/Component/Clock/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Clock Component
2+
===============
3+
4+
Symfony Clock decouples applications from the system clock.
5+
6+
Getting Started
7+
---------------
8+
9+
```
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/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)

0 commit comments

Comments
 (0)
0