Skip to content

Commit 8315f72

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

File tree

12 files changed

+239
-41
lines changed

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 TimePoint
1520+
*/
1521+
- protected function now(): \DateTimeImmutable
1522+
+ protected function now(): TimePoint
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy