8000 [Clock] Add `DatePoint`: an immutable DateTime implementation with st… · symfony/symfony@2f384d1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2f384d1

Browse files
[Clock] Add DatePoint: an immutable DateTime implementation with stricter error handling and return types
1 parent 0659364 commit 2f384d1

12 files changed

+239
-41
lines changed

.github/expected-missing-return-types.diff

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,16 @@ diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/
15121512
+ public function __wakeup(): void
15131513
{
15141514
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
1515+
diff --git a/src/Symfony/Component/Clock/ClockAwareTrait.php b/src/Symfony/Component/Clock/ClockAwareTrait.php
1516+
--- a/src/Symfony/Component/Clock/ClockAwareTrait.php
1517+
+++ b/src/Symfony/Component/Clock/ClockAwareTrait.php
1518+
@@ -33,5 +33,5 @@ trait ClockAwareTrait
1519+
* @return DatePoint
1520+
*/
1521+
- protected function now(): \DateTimeImmutable
1522+
+ protected function now(): DatePoint
1523+
{
1524+
$now = ($this->clock ??= new Clock())->now();
15151525
diff --git a/src/Symfony/Component/Config/ConfigCacheInterface.php b/src/Symfony/Component/Config/ConfigCacheInterface.php
15161526
--- a/src/Symfony/Component/Config/ConfigCacheInterface.php
15171527
+++ b/src/Symfony/Component/Config/ConfigCacheInterface.php

src/Symfony/Component/Clock/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.4
55
---
66

7+
* Add `DatePoint`: an immutable DateTime implementation with stricter error handling and return types
78
* Throw `DateMalformedStringException`/`DateInvalidTimeZoneException` when appropriate
89
* Add `$modifier` argument to the `now()` helper
910

src/Symfony/Component/Clock/Clock.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ public static function set(PsrClockInterface $clock): void
4444
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
4545
}
4646

47-
public function now(): \DateTimeImmutable
47+
public function now(): DatePoint
4848
{
4949
$now = ($this->clock ?? self::get())->now();
5050

51+
if (!$now instanceof DatePoint) {
52+
$now = DatePoint::createFromInterface($now);
53+
}
54+
5155
return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
5256
}
5357

src/Symfony/Component/Clock/ClockAwareTrait.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ public function setClock(ClockInterface $clock): void
2929
$this->clock = $clock;
3030
}
3131

32+
/**
33+
* @return DatePoint
34+
*/
3235
protected function now(): \DateTimeImmutable
3336
{
34-
return ($this->clock ??= new Clock())->now();
37+
$now = ($this->clock ??= new Clock())->now();
38+
39+
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
3540
}
3641
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
* An immmutable DateTime with stricter error handling and return types than the native one.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
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+
$timezone = (new parent($datetime, $timezone ?? $now->getTimezone()))->getTimezone();
36+
} catch (\Exception $e) {
37+
throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e);
38+
}
39+
} else {
40+
$timezone = (new parent($datetime, $timezone ?? $now->getTimezone()))->getTimezone();
41+
}
42+
43+
$now = $now->setTimezone($timezone)->modify($datetime);
44+
} elseif (null !== $timezone) {
45+
$now = $now->setTimezone($timezone);
46+
}
47+
48+
if (\PHP_VERSION_ID < 80200) {
49+
$now = (array) $now;
50+
$this->date = $now['date'];
51+
$this->timezone_type = $now['timezone_type'];
52+
$this->timezone = $now['timezone'];
53+
$this->__wakeup();
54+
55+
return;
56+
}
57+
58+
$this->__unserialize((array) $now);
59+
}
60+
61+
/**
62+
* @throws \DateMalformedStringException When $format or $datetime are invalid
63+
*/
64+
public static function createFromFormat(string $format, string $datetime, \DateTimeZone $timezone = null): static
65+
{
66+
return parent::createFromFormat($format, $datetime, $timezone) ?: throw new \DateMalformedStringException(static::getLastErrors()['errors'][0] ?? 'Invalid date string or format.');
67+
}
68+
69+
public static function createFromInterface(\DateTimeInterface $object): static
70+
{
71+
return parent::createFromInterface($object);
72+
}
73+
74+
public static function createFromMutable(\DateTime $object): static
75+
{
76+
return parent::createFromMutable($object);
77+
}
78+
79+
public function add(\DateInterval $interval): static
80+
{
81+
return parent::add($interval);
82+
}
83+
84+
public function sub(\DateInterval $interval): static
85+
{
86+
return parent::sub($interval);
87+
}
88+
89+
/**
90+
* @throws \DateMalformedStringException When $modifier is invalid
91+
*/
92+
public function modify(string $modifier): static
93+
{
94+
if (\PHP_VERSION_ID < 80300) {
95+
return @parent::modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? sprintf('Invalid modifier: "%s".', $modifier));
96+
}
97+
98+
return parent::modify($modifier);
99+
}
100+
101+
public function setTimestamp(int $value): static
102+
{
103+
return parent::setTimestamp($value);
104+
}
105+
106+
public function setDate(int $year, int $month, int $day): static
107+
{
108+
return parent::setDate($year, $month, $day);
109+
}
110+
111+
public function setISODate(int $year, int $week, int $day = 1): static
112+
{
113+
return parent::setISODate($year, $week, $day);
114+
}
115+
116+
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
117+
{
118+
return parent::setTime($hour, $minute, $second, $microsecond);
119+
}
120+
121+
public function setTimezone(\DateTimeZone $timezone): static
122+
{
123+
return parent::setTimezone($timezone);
124+
}
125+
126+
public function getTimezone(): \DateTimeZone
127+
{
128+
return parent::getTimezone() ?: throw new \DateInvalidTimeZoneException('The DatePoint object has no timezone.');
129+
}
130+
}

src/Symfony/Component/Clock/MockClock.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121
final class MockClock implements ClockInterface
2222
{
23-
private \DateTimeImmutable $now;
23+
private DatePoint $now;
2424

2525
/**
2626
* @throws \DateMalformedStringException When $now is invalid
@@ -38,20 +38,16 @@ public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZon
3838
}
3939
}
4040

41-
if (\PHP_VERSION_ID >= 80300 && \is_string($now)) {
42-
$now = new \DateTimeImmutable($now, $timezone ?? new \DateTimeZone('UTC'));
43-
} elseif (\is_string($now)) {
44-
try {
45-
$now = new \DateTimeImmutable($now, $timezone ?? new \DateTimeZone('UTC'));
46-
} catch (\Exception $e) {
47-
throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e);
48-
}
41+
if (\is_string($now)) {
42+
$now = new DatePoint($now, $timezone ?? new \DateTimeZone('UTC'));
43+
} elseif (!$now instanceof DatePoint) {
44+
$now = DatePoint::createFromInterface($now);
4945
}
5046

5147
$this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
5248
}
5349

54-
public function now(): \DateTimeImmutable
50+
public function now(): DatePoint
5551
{
5652
return clone $this->now;
5753
}
@@ -62,7 +58,7 @@ public function sleep(float|int $seconds): void
6258
$now = substr_replace(sprintf('@%07.0F', $now), '.', -6, 0);
6359
$timezone = $this->now->getTimezone();
6460

65-
$this->now = (new \DateTimeImmutable($now, $timezone))->setTimezone($timezone);
61+
$this->now = DatePoint::createFromInterface(new \DateTimeImmutable($now, $timezone))->setTimezone($timezone);
6662
}
6763

6864
/**

src/Symfony/Component/Clock/MonotonicClock.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function __construct(\DateTimeZone|string $timezone = null)
3838
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
3939
}
4040

41-
public function now(): \DateTimeImmutable
41+
public function now(): DatePoint
4242
{
4343
[$s, $us] = hrtime();
4444

@@ -56,7 +56,7 @@ public function now(): \DateTimeImmutable
5656

5757
$now = '@'.($s + $this->sOffset).'.'.$now;
5858

59-
return (new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
59+
return DatePoint::createFromInterface(new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
6060
}
6161

6262
public function sleep(float|int $seconds): void

src/Symfony/Component/Clock/NativeClock.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ public function __construct(\DateTimeZone|string $timezone = null)
2828
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
2929
}
3030

31-
public function now(): \DateTimeImmutable
31+
public function now(): DatePoint
3232
{
33-
return new \DateTimeImmutable('now', $this->timezone);
33+
return DatePoint::createFromInterface(new \DateTimeImmutable('now', $this->timezone));
3434
}
3535

3636
public function sleep(float|int $seconds): void

src/Symfony/Component/Clock/Resources/now.php

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,14 @@
1515
/**
1616
* @throws \DateMalformedStringException When the modifier is invalid
1717
*/
18-
function now(string $modifier = null): \DateTimeImmutable
18+
function now(string $modifier = 'now'): DatePoint
1919
{
20-
if (null === $modifier || 'now' === $modifier) {
21-
return Clock::get()->now();
20+
if ('now' !== $modifier) {
21+
return new DatePoint($modifier);
2222
}
2323

2424
$now = Clock::get()->now();
2525

26-
if (\PHP_VERSION_ID < 80300) {
27-
try {
28-
$tz = (new \DateTimeImmutable($modifier, $now->getTimezone()))->getTimezone();
29-
} catch (\Exception $e) {
30-
throw new \DateMalformedStringException($e->getMessage(), $e->getCode(), $e);
31-
}
32-
$now = $now->setTimezone($tz);
33-
34-
return @$now->modify($modifier) ?: throw new \DateMalformedStringException(error_get_last()['message'] ?? sprintf('Invalid date modifier "%s".', $modifier));
35-
}
36-
37-
$tz = (new \DateTimeImmutable($modifier, $now->getTimezone()))->getTimezone();
38-
39-
return $now->setTimezone($tz)->modify($modifier);
26+
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
4027
}
4128
}

src/Symfony/Component/Clock/Tests/ClockAwareTraitTest.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,16 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Clock\ClockAwareTrait;
16+
use Symfony\Component\Clock\DatePoint;
1617
use Symfony\Component\Clock\MockClock;
1718

1819
class ClockAwareTraitTest extends TestCase
1920
{
2021
public function testTrait()
2122
{
22-
$sut = new class() {
23-
use ClockAwareTrait {
24-
now as public;
25-
}
26-
};
23+
$sut = new ClockAwareTestImplem();
2724

28-
$this->assertInstanceOf(\DateTimeImmutable::class, $sut->now());
25+
$this->assertInstanceOf(DatePoint::class, $sut->now());
2926

3027
$clock = new MockClock();
3128
$sut = new $sut();
@@ -38,3 +35,10 @@ public function testTrait()
3835
$this->assertSame(1.0, round($sut->now()->getTimestamp() - $ts, 1));
3936
}
4037
}
38+
39+
class ClockAwareTestImplem
40+
{
41+
use ClockAwareTrait {
42+
now as public;
43+
}
44+
}

src/Symfony/Component/Clock/Tests/ClockTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Clock\ClockInterface;
1616
use Symfony\Component\Clock\Clock;
17+
use Symfony\Component\Clock\DatePoint;
1718
use Symfony\Component\Clock\MockClock;
1819
use Symfony\Component\Clock\NativeClock;
1920
use Symfony\Component\Clock\Test\ClockSensitiveTrait;
@@ -35,7 +36,7 @@ public function testMockClock()
3536

3637
public function testNativeClock()
3738
{
38-
$this->assertInstanceOf(\DateTimeImmutable::class, now());
39+
$this->assertInstanceOf(DatePoint::class, now());
3940
$this->assertInstanceOf(NativeClock::class, Clock::get());
4041
}
4142

0 commit comments

Comments
 (0)
0