Skip to content

Commit 7390dac

Browse files
committed
feature #23747 [Serializer][FrameworkBundle] Add a DateInterval normalizer (Lctrs)
This PR was squashed before being merged into the 3.4 branch (closes #23747). Discussion ---------- [Serializer][FrameworkBundle] Add a DateInterval normalizer | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | symfony/symfony-docs#8267 Could be useful for API needing to submit a duration. Most code have been adapted from @MisatoTremor's DateInterval form type. Credits to him. Commits ------- 6185cb1991 [Serializer][FrameworkBundle] Add a DateInterval normalizer
2 parents ebb41ae + 3c653d0 commit 7390dac

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

CHANGELOG.md

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

77
* added `AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT` context option
88
to disable throwing an `UnexpectedValueException` on a type mismatch
9+
* added support for serializing `DateInterval` objects
910

1011
3.3.0
1112
-----

Normalizer/DateIntervalNormalizer.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Serializer\Normalizer;
13+
14+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
15+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
16+
17+
/**
18+
* Normalizes an instance of {@see \DateInterval} to an interval string.
19+
* Denormalizes an interval string to an instance of {@see \DateInterval}.
20+
*
21+
* @author Jérôme Parmentier <[email protected]>
22+
*/
23+
class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface
24+
{
25+
const FORMAT_KEY = 'dateinterval_format';
26+
27+
/**
28+
* @var string
29+
*/
30+
private $format;
31+
32+
/**
33+
* @param string $format
34+
*/
35+
public function __construct($format = 'P%yY%mM%dDT%hH%iM%sS')
36+
{
37+
$this->format = $format;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*
43+
* @throws InvalidArgumentException
44+
*/
45+
public function normalize($object, $format = null, array $context = array())
46+
{
47+
if (!$object instanceof \DateInterval) {
48+
throw new InvalidArgumentException('The object must be an instance of "\DateInterval".');
49+
}
50+
51+
$dateIntervalFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
52+
53+
return $object->format($dateIntervalFormat);
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function supportsNormalization($data, $format = null)
60+
{
61+
return $data instanceof \DateInterval;
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*
67+
* @throws InvalidArgumentException
68+
* @throws UnexpectedValueException
69+
*/
70+
public function denormalize($data, $class, $format = null, array $context = array())
71+
{
72+
if (!is_string($data)) {
73+
throw new InvalidArgumentException(sprintf('Data expected to be a string, %s given.', gettype($data)));
74+
}
75+
76+
if (!$this->isISO8601($data)) {
77+
throw new UnexpectedValueException('Expected a valid ISO 8601 interval string.');
78+
}
79+
80+
$dateIntervalFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
81+
82+
$valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $dateIntervalFormat).'$/';
83+
if (!preg_match($valuePattern, $data)) {
84+
throw new UnexpectedValueException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat));
85+
}
86+
87+
try {
88+
return new \DateInterval($data);
89+
} catch (\Exception $e) {
90+
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
91+
}
92+
}
93+
94+
/**
95+
* {@inheritdoc}
96+
*/
97+
public function supportsDenormalization($data, $type, $format = null)
98+
{
99+
return \DateInterval::class === $type;
100+
}
101+
102+
private function isISO8601($string)
103+
{
104+
return preg_match('/^P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
105+
}
106+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Symfony\Component\Serializer\Tests\Normalizer;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer;
7+
8+
/**
9+
* @author Jérôme Parmentier <[email protected]>
10+
*/
11+
class DateIntervalNormalizerTest extends TestCase
12+
{
13+
/**
14+
* @var DateIntervalNormalizer
15+
*/
16+
private $normalizer;
17+
18+
protected function setUp()
19+
{
20+
$this->normalizer = new DateIntervalNormalizer();
21+
}
22+
23+
public function dataProviderISO()
24+
{
25+
$data = array(
26+
array('P%YY%MM%DDT%HH%IM%SS', 'P00Y00M00DT00H00M00S', 'PT0S'),
27+
array('P%yY%mM%dDT%hH%iM%sS', 'P0Y0M0DT0H0M0S', 'PT0S'),
28+
array('P%yY%mM%dDT%hH%iM%sS', 'P10Y2M3DT16H5M6S', 'P10Y2M3DT16H5M6S'),
29+
array('P%yY%mM%dDT%hH%iM', 'P10Y2M3DT16H5M', 'P10Y2M3DT16H5M'),
30+
array('P%yY%mM%dDT%hH', 'P10Y2M3DT16H', 'P10Y2M3DT16H'),
31+
array('P%yY%mM%dD', 'P10Y2M3D', 'P10Y2M3DT0H'),
32+
);
33+
34+
return $data;
35+
}
36+
37+
public function testSupportsNormalization()
38+
{
39+
$this->assertTrue($this->normalizer->supportsNormalization(new \DateInterval('P00Y00M00DT00H00M00S')));
40+
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
41+
}
42+
43+
public function testNormalize()
44+
{
45+
$this->assertEquals('P0Y0M0DT0H0M0S', $this->normalizer->normalize(new \DateInterval('PT0S')));
46+
}
47+
48+
/**
49+
* @dataProvider dataProviderISO
50+
*/
51+
public function testNormalizeUsingFormatPassedInContext($format, $output, $input)
52+
{
53+
$this->assertEquals($output, $this->normalizer->normalize(new \DateInterval($input), null, array(DateIntervalNormalizer::FORMAT_KEY => $format)));
54+
}
55+
56+
/**
57+
* @dataProvider dataProviderISO
58+
*/
59+
public function testNormalizeUsingFormatPassedInConstructor($format, $output, $input)
60+
{
61+
$this->assertEquals($output, (new DateIntervalNormalizer($format))->normalize(new \DateInterval($input)));
62+
}
63+
64+
/**
65+
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
66+
* @expectedExceptionMessage The object must be an instance of "\DateInterval".
67+
*/
68+
public function testNormalizeInvalidObjectThrowsException()
69+
{
70+
$this->normalizer->normalize(new \stdClass());
71+
}
72+
73+
public function testSupportsDenormalization()
74+
{
75+
$this->assertTrue($this->normalizer->supportsDenormalization('P00Y00M00DT00H00M00S', \DateInterval::class));
76+
$this->assertFalse($this->normalizer->supportsDenormalization('foo', 'Bar'));
77+
}
78+
79+
public function testDenormalize()
80+
{
81+
$this->assertDateIntervalEquals(new \DateInterval('P00Y00M00DT00H00M00S'), $this->normalizer->denormalize('P00Y00M00DT00H00M00S', \DateInterval::class));
82+
}
83+
84+
/**
85+
* @dataProvider dataProviderISO
86+
*/
87+
public function testDenormalizeUsingFormatPassedInContext($format, $input, $output)
88+
{
89+
$this->assertDateIntervalEquals(new \DateInterval($output), $this->normalizer->denormalize($input, \DateInterval::class, null, array(DateIntervalNormalizer::FORMAT_KEY => $format)));
90+
}
91+
92+
/**
93+
* @dataProvider dataProviderISO
94+
*/
95+
public function testDenormalizeUsingFormatPassedInConstructor($format, $input, $output)
96+
{
97+
$this->assertDateIntervalEquals(new \DateInterval($output), (new DateIntervalNormalizer($format))->denormalize($input, \DateInterval::class));
98+
}
99+
100+
/**
101+
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
102+
*/
103+
public function testDenormalizeExpectsString()
104+
{
105+
$this->normalizer->denormalize(1234, \DateInterval::class);
106+
}
107+
108+
/**
109+
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
110+
* @expectedExceptionMessage Expected a valid ISO 8601 interval string.
111+
*/
112+
public function testDenormalizeNonISO8601IntervalStringThrowsException()
113+
{
114+
$this->normalizer->denormalize('10 years 2 months 3 days', \DateInterval::class, null);
115+
}
116+
117+
/**
118+
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
119+
*/
120+
public function testDenormalizeInvalidDataThrowsException()
121+
{
122+
$this->normalizer->denormalize('invalid interval', \DateInterval::class);
123+
}
124+
125+
/**
126+
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
127+
*/
128+
public function testDenormalizeFormatMismatchThrowsException()
129+
{
130+
$this->normalizer->denormalize('P00Y00M00DT00H00M00S', \DateInterval::class, null, array(DateIntervalNormalizer::FORMAT_KEY => 'P%yY%mM%dD'));
131+
}
132+
133+
private function assertDateIntervalEquals(\DateInterval $expected, \DateInterval $actual)
134+
{
135+
$this->assertEquals($expected->format('%RP%yY%mM%dDT%hH%iM%sS'), $actual->format('%RP%yY%mM%dDT%hH%iM%sS'));
136+
}
137+
}

0 commit comments

Comments
 (0)