Skip to content

Commit 27f537a

Browse files
authored
Merge pull request #141 from gsteel/date-interval-normalizer
Introduce `DateIntervalNormalizer`
2 parents 7fcdc62 + a552d01 commit 27f537a

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

src/Guesser/BuiltInGuesser.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
namespace Patchlevel\Hydrator\Guesser;
66

7+
use DateInterval;
78
use DateTime;
89
use DateTimeImmutable;
910
use DateTimeZone;
11+
use Patchlevel\Hydrator\Normalizer\DateIntervalNormalizer;
1012
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
1113
use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;
1214
use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;
@@ -30,6 +32,7 @@ public function guess(ObjectType $type): Normalizer|null
3032
}
3133

3234
return match ($type->getClassName()) {
35+
DateInterval::class => new DateIntervalNormalizer(),
3336
DateTimeImmutable::class => new DateTimeImmutableNormalizer(),
3437
DateTime::class => new DateTimeNormalizer(),
3538
DateTimeZone::class => new DateTimeZoneNormalizer(),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Normalizer;
6+
7+
use Attribute;
8+
use DateInterval;
9+
use Throwable;
10+
11+
use function is_string;
12+
13+
#[Attribute(Attribute::TARGET_PROPERTY)]
14+
final readonly class DateIntervalNormalizer implements Normalizer
15+
{
16+
private const DEFAULT_FORMAT = 'P%YY%MM%DDT%HH%IM%SS';
17+
18+
public function __construct(private string $format = self::DEFAULT_FORMAT)
19+
{
20+
}
21+
22+
public function normalize(mixed $value): string|null
23+
{
24+
if ($value === null) {
25+
return null;
26+
}
27+
28+
if (!$value instanceof DateInterval) {
29+
throw InvalidArgument::withWrongType('DateInterval|null', $value);
30+
}
31+
32+
return $value->format($this->format);
33+
}
34+
35+
public function denormalize(mixed $value): DateInterval|null
36+
{
37+
if ($value === null) {
38+
return null;
39+
}
40+
41+
if (!is_string($value)) {
42+
throw InvalidArgument::withWrongType('string|null', $value);
43+
}
44+
45+
try {
46+
$interval = new DateInterval($value);
47+
} catch (Throwable $e) { // Exception in PHP <= 8.2 or DateMalformedIntervalStringException in 8.3+
48+
throw new InvalidArgument('Invalid serialized date interval string', 0, $e);
49+
}
50+
51+
return $interval;
52+
}
53+
}

tests/Unit/Guesser/BuiltInGuesserTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
namespace Patchlevel\Hydrator\Tests\Unit\Guesser;
66

7+
use DateInterval;
78
use DateTime;
89
use DateTimeImmutable;
910
use DateTimeZone;
1011
use Patchlevel\Hydrator\Guesser\BuiltInGuesser;
12+
use Patchlevel\Hydrator\Normalizer\DateIntervalNormalizer;
1113
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
1214
use Patchlevel\Hydrator\Normalizer\DateTimeNormalizer;
1315
use Patchlevel\Hydrator\Normalizer\DateTimeZoneNormalizer;
@@ -62,6 +64,15 @@ public function testDateTimeZone(): void
6264
);
6365
}
6466

67+
public function testDateInterval(): void
68+
{
69+
$guesser = new BuiltInGuesser();
70+
self::assertInstanceOf(
71+
DateIntervalNormalizer::class,
72+
$guesser->guess(Type::object(DateInterval::class)),
73+
);
74+
}
75+
6576
public function testFallbackObjectNormalizer(): void
6677
{
6778
$guesser = new BuiltInGuesser();
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Tests\Unit\Normalizer;
6+
7+
use DateInterval;
8+
use Patchlevel\Hydrator\Normalizer\DateIntervalNormalizer;
9+
use Patchlevel\Hydrator\Normalizer\InvalidArgument;
10+
use PHPUnit\Framework\TestCase;
11+
12+
final class DateIntervalNormalizerTest extends TestCase
13+
{
14+
public function testNormalizeWithNull(): void
15+
{
16+
$normalizer = new DateIntervalNormalizer();
17+
self::assertNull($normalizer->normalize(null));
18+
}
19+
20+
public function testDenormalizeWithNull(): void
21+
{
22+
$normalizer = new DateIntervalNormalizer();
23+
self::assertNull($normalizer->denormalize(null));
24+
}
25+
26+
public function testNormalizeWithInvalidArgument(): void
27+
{
28+
$this->expectException(InvalidArgument::class);
29+
$this->expectExceptionCode(0);
30+
31+
$normalizer = new DateIntervalNormalizer();
32+
$normalizer->normalize(123);
33+
}
34+
35+
public function testDenormalizeWithInvalidArgument(): void
36+
{
37+
$this->expectException(InvalidArgument::class);
38+
$this->expectExceptionCode(0);
39+
40+
$normalizer = new DateIntervalNormalizer();
41+
$normalizer->denormalize(123);
42+
}
43+
44+
public function testNormalizeWithValue(): void
45+
{
46+
$normalizer = new DateIntervalNormalizer();
47+
self::assertSame('P02Y02M25DT06H07M08S', $normalizer->normalize(new DateInterval('P2Y2M3W4DT6H7M8S')));
48+
}
49+
50+
public function testNormalizeWithChangeFormat(): void
51+
{
52+
$normalizer = new DateIntervalNormalizer(format: 'P%YY%MM');
53+
self::assertSame('P02Y02M', $normalizer->normalize(new DateInterval('P2Y2M3W4DT6H7M8S')));
54+
}
55+
56+
public function testDenormalizeWithValue(): void
57+
{
58+
$normalizer = new DateIntervalNormalizer();
59+
$denormalized = $normalizer->denormalize('P00Y00M35DT00H00M00S');
60+
self::assertNotNull($denormalized);
61+
62+
$this->assertEqualInterval(
63+
new DateInterval('P5W'),
64+
$denormalized,
65+
);
66+
}
67+
68+
public function testDenormalizeWithChangeFormat(): void
69+
{
70+
$normalizer = new DateIntervalNormalizer(format: 'P%YY');
71+
$denormalized = $normalizer->denormalize('P5Y');
72+
self::assertNotNull($denormalized);
73+
74+
$this->assertEqualInterval(
75+
new DateInterval('P5Y'),
76+
$denormalized,
77+
);
78+
}
79+
80+
public function testDateIntervalErrorsAreCaughtAndReThrown(): void
81+
{
82+
$this->expectException(InvalidArgument::class);
83+
$this->expectExceptionMessage('Invalid serialized date interval string');
84+
$this->expectExceptionCode(0);
85+
86+
(new DateIntervalNormalizer())->denormalize('Kermit');
87+
}
88+
89+
private function assertEqualInterval(DateInterval $a, DateInterval $b): void
90+
{
91+
self::assertSame($a->y, $b->y);
92+
self::assertSame($a->m, $b->m);
93+
self::assertSame($a->d, $b->d);
94+
self::assertSame($a->h, $b->h);
95+
self::assertSame($a->i, $b->i);
96+
self::assertSame($a->s, $b->s);
97+
self::assertSame($a->invert, $b->invert);
98+
self::assertSame($a->f, $b->f);
99+
self::assertSame($a->days, $b->days);
100+
}
101+
}

0 commit comments

Comments
 (0)