Skip to content

Commit c39a39b

Browse files
committed
[HttpKernel] Add DateTimeValueResolver
1 parent 1a08c65 commit c39a39b

File tree

4 files changed

+271
-0
lines changed

4 files changed

+271
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver;
1515
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
1616
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver;
17+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
1718
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
1819
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
1920
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
@@ -56,6 +57,9 @@
5657
'priority' => 100, // same priority than RequestAttributeValueResolver, but registered before
5758
])
5859

60+
->set('argument_resolver.datetime', DateTimeValueResolver::class)
61+
->tag('controller.argument_value_resolver', ['priority' => 105])
62+
5963
->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class)
6064
->tag('controller.argument_value_resolver', ['priority' => 100])
6165

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\HttpKernel\Attribute;
13+
14+
/**
15+
* Controller parameter tag to configure DateTime arguments.
16+
*/
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class MapDateTime
19+
{
20+
public function __construct(
21+
public readonly ?string $format = null
22+
) {
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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\HttpKernel\Controller\ArgumentResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Attribute\MapDateTime;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
17+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
19+
20+
/**
21+
* Convert DateTime instances from request attribute variable.
22+
*
23+
* @author Benjamin Eberlei <kontakt@beberlei.de>
24+
* @author Tim Goudriaan <tim@codedmonkey.com>
25+
*/
26+
final class DateTimeValueResolver implements ArgumentValueResolverInterface
27+
{
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function supports(Request $request, ArgumentMetadata $argument): bool
32+
{
33+
return is_a($argument->getType(), \DateTimeInterface::class, true) && $request->attributes->has($argument->getName());
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
40+
{
41+
$value = $request->attributes->get($argument->getName());
42+
43+
if ($argument->isNullable() && !$value) {
44+
yield null;
45+
46+
return;
47+
}
48+
49+
$class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType();
50+
$format = null;
51+
52+
if ($attributes = $argument->getAttributes(MapDateTime::class, ArgumentMetadata::IS_INSTANCEOF)) {
53+
$attribute = $attributes[0];
54+
$format = $attribute->format;
55+
}
56+
57+
$date = false;
58+
59+
if (null !== $format) {
60+
$date = $class::createFromFormat($format, $value);
61+
62+
if (0 < \DateTime::getLastErrors()['warning_count']) {
63+
$date = false;
64+
}
65+
} elseif (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
66+
$date = new $class('@'.$value);
67+
} elseif (false !== $timestamp = strtotime($value)) {
68+
$date = new $class('@'.$timestamp);
69+
}
70+
71+
if (!$date) {
72+
throw new NotFoundHttpException(sprintf('Invalid date given for parameter "%s".', $argument->getName()));
73+
}
74+
75+
yield $date;
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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\HttpKernel\Tests\Controller\ArgumentResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Attribute\MapDateTime;
17+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver;
18+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
19+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
20+
21+
class DateTimeValueResolverTest extends TestCase
22+
{
23+
public function testSupports()
24+
{
25+
$resolver = new DateTimeValueResolver();
26+
27+
$argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null);
28+
$request = self::requestWithAttributes(['dummy' => 'now']);
29+
$this->assertTrue($resolver->supports($request, $argument));
30+
31+
$argument = new ArgumentMetadata('dummy', FooDateTime::class, false, false, null);
32+
$request = self::requestWithAttributes(['dummy' => 'now']);
33+
$this->assertTrue($resolver->supports($request, $argument));
34+
35+
$argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null);
36+
$request = self::requestWithAttributes(['dummy' => 'now']);
37+
$this->assertFalse($resolver->supports($request, $argument));
38+
}
39+
40+
public function testFullDate()
41+
{
42+
$resolver = new DateTimeValueResolver();
43+
44+
$argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null);
45+
$request = self::requestWithAttributes(['dummy' => '2012-07-21 00:00:00']);
46+
47+
/** @var \Generator $results */
48+
$results = $resolver->resolve($request, $argument);
49+
$results = iterator_to_array($results);
50+
51+
$this->assertCount(1, $results);
52+
$this->assertInstanceOf(\DateTime::class, $results[0]);
53+
$this->assertEquals('2012-07-21', $results[0]->format('Y-m-d'));
54+
}
55+
56+
public function testUnixTimestamp()
57+
{
58+
$resolver = new DateTimeValueResolver();
59+
60+
$argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null);
61+
$request = self::requestWithAttributes(['dummy' => '989541720']);
62+
63+
/** @var \Generator $results */
64+
$results = $resolver->resolve($request, $argument);
65+
$results = iterator_to_array($results);
66+
67+
$this->assertCount(1, $results);
68+
$this->assertInstanceOf(\DateTime::class, $results[0]);
69+
$this->assertEquals('2001-05-11', $results[0]->format('Y-m-d'));
70+
}
71+
72+
public function testNullableWithEmptyAttribute()
73+
{
74+
$resolver = new DateTimeValueResolver();
75+
76+
$argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null, true);
77+
$request = self::requestWithAttributes(['dummy' => '']);
78+
79+
/** @var \Generator $results */
80+
$results = $resolver->resolve($request, $argument);
81+
$results = iterator_to_array($results);
82+
83+
$this->assertCount(1, $results);
84+
$this->assertNull($results[0]);
85+
}
86+
87+
public function testCustomClass()
88+
{
89+
$resolver = new DateTimeValueResolver();
90+
91+
$argument = new ArgumentMetadata('dummy', FooDateTime::class, false, false, null);
92+
$request = self::requestWithAttributes(['dummy' => '2016-09-08 00:00:00']);
93+
94+
/** @var \Generator $results */
95+
$results = $resolver->resolve($request, $argument);
96+
$results = iterator_to_array($results);
97+
98+
$this->assertCount(1, $results);
99+
$this->assertInstanceOf(FooDateTime::class, $results[0]);
100+
$this->assertEquals('2016-09-08', $results[0]->format('Y-m-d'));
101+
}
102+
103+
public function testDateTimeImmutable()
104+
{
105+
$resolver = new DateTimeValueResolver();
106+
107+
$argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null);
108+
$request = self::requestWithAttributes(['dummy' => '2016-09-08 00:00:00']);
109+
110+
/** @var \Generator $results */
111+
$results = $resolver->resolve($request, $argument);
112+
$results = iterator_to_array($results);
113+
114+
$this->assertCount(1, $results);
115+
$this->assertInstanceOf(\DateTimeImmutable::class, $results[0]);
116+
$this->assertEquals('2016-09-08', $results[0]->format('Y-m-d'));
117+
}
118+
119+
public function provideInvalidDates()
120+
{
121+
return [
122+
'invalid date' => [
123+
new ArgumentMetadata('dummy', \DateTime::class, false, false, null),
124+
self::requestWithAttributes(['dummy' => 'Invalid DateTime Format'])
125+
],
126+
'invalid format' => [
127+
new ArgumentMetadata('dummy', \DateTime::class, false, false, null, false, [new MapDateTime(format: 'd.m.Y')]),
128+
self::requestWithAttributes(['dummy' => '2012-07-21']),
129+
],
130+
'invalid ymd format' => [
131+
new ArgumentMetadata('dummy', \DateTime::class, false, false, null, false, [new MapDateTime(format: 'Y-m-d')]),
132+
self::requestWithAttributes(['dummy' => '2012-21-07']),
133+
],
134+
];
135+
}
136+
137+
/**
138+
* @dataProvider provideInvalidDates
139+
*/
140+
public function test404Exception(ArgumentMetadata $argument, Request $request)
141+
{
142+
$resolver = new DateTimeValueResolver();
143+
144+
$this->expectException(NotFoundHttpException::class);
145+
$this->expectExceptionMessage('Invalid date given for parameter "dummy".');
146+
147+
/** @var \Generator $results */
148+
$results = $resolver->resolve($request, $argument);
149+
iterator_to_array($results);
150+
}
151+
152+
private static function requestWithAttributes(array $attributes): Request
153+
{
154+
$request = Request::create('/');
155+
156+
foreach ($attributes as $name => $value) {
157+
$request->attributes->set($name, $value);
158+
}
159+
160+
return $request;
161+
}
162+
}
163+
164+
class FooDateTime extends \DateTime
165+
{
166+
}

0 commit comments

Comments
 (0)