Skip to content

Commit fd28f9e

Browse files
committed
wip
1 parent 7df972d commit fd28f9e

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Dbp\Relay\CoreBundle\Doctrine;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
use Doctrine\DBAL\Types\DateImmutableType;
9+
use Doctrine\DBAL\Types\Exception\InvalidFormat;
10+
use Doctrine\DBAL\Types\Exception\InvalidType;
11+
12+
/**
13+
* Custom Doctrine type that extends {@see DateImmutableType} to ensure all date values
14+
* are stored and retrieved in UTC timezone.
15+
*
16+
* This type guarantees timezone consistency by:
17+
* - Converting date values to UTC before persisting to the database
18+
* - Parsing and returning date values in UTC when reading from the database
19+
*/
20+
class DateImmutableUtcType extends DateImmutableType
21+
{
22+
private static \DateTimeZone $utc;
23+
24+
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
25+
{
26+
if ($value instanceof \DateTimeImmutable) {
27+
$value = $value->setTimezone(self::getUtc());
28+
}
29+
30+
return parent::convertToDatabaseValue($value, $platform);
31+
}
32+
33+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?\DateTimeImmutable
34+
{
35+
if ($value === null) {
36+
return null;
37+
}
38+
39+
if ($value instanceof \DateTimeImmutable) {
40+
return $value->setTimezone(self::getUtc());
41+
}
42+
43+
if ($value instanceof \DateTime) {
44+
throw InvalidType::new(
45+
$value,
46+
static::class,
47+
['null', 'string', \DateTimeImmutable::class],
48+
);
49+
}
50+
51+
$dateTime = \DateTimeImmutable::createFromFormat(
52+
'!'.$platform->getDateFormatString(),
53+
$value,
54+
self::getUtc(),
55+
);
56+
57+
if ($dateTime === false) {
58+
throw InvalidFormat::new(
59+
$value,
60+
static::class,
61+
$platform->getDateFormatString(),
62+
);
63+
}
64+
65+
return $dateTime;
66+
}
67+
68+
private static function getUtc(): \DateTimeZone
69+
{
70+
return self::$utc ??= new \DateTimeZone('UTC');
71+
}
72+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Dbp\Relay\CoreBundle\Tests\Doctrine;
6+
7+
use Dbp\Relay\CoreBundle\Doctrine\DateImmutableUtcType;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\ConversionException;
10+
use PHPUnit\Framework\MockObject\MockObject;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class DateImmutableUtcTypeTest extends TestCase
14+
{
15+
private AbstractPlatform&MockObject $platform;
16+
private DateImmutableUtcType $type;
17+
18+
protected function setUp(): void
19+
{
20+
$this->platform = $this->createMock(AbstractPlatform::class);
21+
$this->type = new DateImmutableUtcType();
22+
}
23+
24+
public function testConvertsDateTimeImmutableInstanceToDatabaseValue(): void
25+
{
26+
$date = new \DateTimeImmutable('2016-01-01 12:34:56', new \DateTimeZone('UTC'));
27+
28+
$this->platform->expects(self::once())
29+
->method('getDateFormatString')
30+
->willReturn('Y-m-d');
31+
32+
self::assertSame(
33+
'2016-01-01',
34+
$this->type->convertToDatabaseValue($date, $this->platform),
35+
);
36+
}
37+
38+
public function testConvertsDateTimeImmutableInDifferentTimezoneToDatabaseValueInUtc(): void
39+
{
40+
// 2016-01-01 02:00:00 in +05:00 is 2015-12-31 21:00:00 UTC
41+
$date = new \DateTimeImmutable('2016-01-01 02:00:00', new \DateTimeZone('+05:00'));
42+
43+
$this->platform->expects(self::once())
44+
->method('getDateFormatString')
45+
->willReturn('Y-m-d');
46+
47+
self::assertSame(
48+
'2015-12-31',
49+
$this->type->convertToDatabaseValue($date, $this->platform),
50+
);
51+
}
52+
53+
public function testConvertsNullToDatabaseValue(): void
54+
{
55+
self::assertNull($this->type->convertToDatabaseValue(null, $this->platform));
56+
}
57+
58+
public function testDoesNotSupportMutableDateTimeToDatabaseValueConversion(): void
59+
{
60+
$this->expectException(ConversionException::class);
61+
62+
$this->type->convertToDatabaseValue(new \DateTime(), $this->platform);
63+
}
64+
65+
public function testConvertsDateTimeImmutableInstanceToPHPValueInUtc(): void
66+
{
67+
$date = new \DateTimeImmutable('2016-01-01 12:00:00', new \DateTimeZone('+05:00'));
68+
69+
$result = $this->type->convertToPHPValue($date, $this->platform);
70+
71+
self::assertInstanceOf(\DateTimeImmutable::class, $result);
72+
self::assertSame('UTC', $result->getTimezone()->getName());
73+
self::assertSame('2016-01-01 07:00:00', $result->format('Y-m-d H:i:s'));
74+
}
75+
76+
public function testConvertsNullToPHPValue(): void
77+
{
78+
self::assertNull($this->type->convertToPHPValue(null, $this->platform));
79+
}
80+
81+
public function testConvertsDateStringToPHPValue(): void
82+
{
83+
$this->platform->expects(self::once())
84+
->method('getDateFormatString')
85+
->willReturn('Y-m-d');
86+
87+
$date = $this->type->convertToPHPValue('2016-01-01', $this->platform);
88+
89+
self::assertInstanceOf(\DateTimeImmutable::class, $date);
90+
self::assertSame('2016-01-01', $date->format('Y-m-d'));
91+
self::assertSame('UTC', $date->getTimezone()->getName());
92+
}
93+
94+
public function testResetTimeFractionsWhenConvertingToPHPValue(): void
95+
{
96+
$this->platform
97+
->method('getDateFormatString')
98+
->willReturn('Y-m-d');
99+
100+
$date = $this->type->convertToPHPValue('2016-01-01', $this->platform);
101+
102+
self::assertNotNull($date);
103+
self::assertSame('2016-01-01 00:00:00.000000', $date->format('Y-m-d H:i:s.u'));
104+
}
105+
106+
public function testConvertedPHPValueAlwaysHasUtcTimezone(): void
107+
{
108+
$this->platform
109+
->method('getDateFormatString')
110+
->willReturn('Y-m-d');
111+
112+
$date = $this->type->convertToPHPValue('2016-06-15', $this->platform);
113+
114+
self::assertNotNull($date);
115+
self::assertSame('UTC', $date->getTimezone()->getName());
116+
}
117+
118+
public function testThrowsExceptionDuringConversionToPHPValueWithInvalidDateString(): void
119+
{
120+
$this->expectException(ConversionException::class);
121+
122+
$this->platform
123+
->method('getDateFormatString')
124+
->willReturn('Y-m-d');
125+
126+
$this->type->convertToPHPValue('invalid date string', $this->platform);
127+
}
128+
129+
public function testThrowsExceptionWhenConvertingMutableDateTimeToPHPValue(): void
130+
{
131+
$this->expectException(ConversionException::class);
132+
133+
$this->type->convertToPHPValue(new \DateTime(), $this->platform);
134+
}
135+
136+
public function testConvertToDatabaseValuePreservesUtcDate(): void
137+
{
138+
$date = new \DateTimeImmutable('2023-07-15 00:00:00', new \DateTimeZone('UTC'));
139+
140+
$this->platform->expects(self::once())
141+
->method('getDateFormatString')
142+
->willReturn('Y-m-d');
143+
144+
self::assertSame(
145+
'2023-07-15',
146+
$this->type->convertToDatabaseValue($date, $this->platform),
147+
);
148+
}
149+
150+
public function testConvertToPHPValueWithDateTimeImmutableAlreadyInUtc(): void
151+
{
152+
$date = new \DateTimeImmutable('2023-07-15 10:30:00', new \DateTimeZone('UTC'));
153+
154+
$result = $this->type->convertToPHPValue($date, $this->platform);
155+
156+
self::assertInstanceOf(\DateTimeImmutable::class, $result);
157+
self::assertSame('UTC', $result->getTimezone()->getName());
158+
self::assertSame('2023-07-15 10:30:00', $result->format('Y-m-d H:i:s'));
159+
}
160+
161+
public function testRoundTripConversionPreservesDateInUtc(): void
162+
{
163+
$this->platform
164+
->method('getDateFormatString')
165+
->willReturn('Y-m-d');
166+
167+
$original = new \DateTimeImmutable('2023-12-25 00:00:00', new \DateTimeZone('UTC'));
168+
169+
$dbValue = $this->type->convertToDatabaseValue($original, $this->platform);
170+
$phpValue = $this->type->convertToPHPValue($dbValue, $this->platform);
171+
172+
self::assertNotNull($phpValue);
173+
self::assertSame('2023-12-25', $phpValue->format('Y-m-d'));
174+
self::assertSame('UTC', $phpValue->getTimezone()->getName());
175+
}
176+
177+
public function testRoundTripConversionWithNonUtcTimezone(): void
178+
{
179+
$this->platform
180+
->method('getDateFormatString')
181+
->willReturn('Y-m-d');
182+
183+
// 2023-01-01 01:00:00 in +05:00 is 2022-12-31 20:00:00 UTC
184+
$original = new \DateTimeImmutable('2023-01-01 01:00:00', new \DateTimeZone('+05:00'));
185+
186+
$dbValue = $this->type->convertToDatabaseValue($original, $this->platform);
187+
self::assertSame('2022-12-31', $dbValue);
188+
189+
$phpValue = $this->type->convertToPHPValue($dbValue, $this->platform);
190+
191+
self::assertNotNull($phpValue);
192+
self::assertSame('2022-12-31', $phpValue->format('Y-m-d'));
193+
self::assertSame('UTC', $phpValue->getTimezone()->getName());
194+
}
195+
}

0 commit comments

Comments
 (0)