diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 82ceb2e077da4..cd4cdffa10ade 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -55,6 +55,9 @@ ->tag('controller.argument_value_resolver', ['priority' => 100]) ->set('argument_resolver.datetime', DateTimeValueResolver::class) + ->args([ + service('clock')->nullOnInvalid(), + ]) ->tag('controller.argument_value_resolver', ['priority' => 100]) ->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class) diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 02c9da3d12b0e..c823fbd0cb5c4 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Deprecate parameters `container.dumper.inline_factories` and `container.dumper.inline_class_loader`, use `.container.dumper.inline_factories` and `.container.dumper.inline_class_loader` instead * `FileProfilerStorage` removes profiles automatically after two days * Add `#[HttpStatus]` for defining status codes for exceptions + * Use an instance of `Psr\Clock\ClockInterface` to generate the current date time in `DateTimeValueResolver` 6.2 --- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php index 8fd7015ad041d..a73a7e1b47a27 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DateTimeValueResolver.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Psr\Clock\ClockInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -26,6 +27,11 @@ */ final class DateTimeValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface { + public function __construct( + private readonly ?ClockInterface $clock = null, + ) { + } + /** * @deprecated since Symfony 6.2, use resolve() instead */ @@ -45,12 +51,18 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $value = $request->attributes->get($argument->getName()); $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); - if ($value instanceof \DateTimeInterface) { - return [$value instanceof $class ? $value : $class::createFromInterface($value)]; + if (!$value) { + if ($argument->isNullable()) { + return [null]; + } + if (!$this->clock) { + return [new $class()]; + } + $value = $this->clock->now(); } - if ($argument->isNullable() && !$value) { - return [null]; + if ($value instanceof \DateTimeInterface) { + return [$value instanceof $class ? $value : $class::createFromInterface($value)]; } $format = null; @@ -71,7 +83,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $value = '@'.$value; } try { - $date = new $class($value ?? 'now'); + $date = new $class($value); } catch (\Exception) { $date = false; } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php index 770324d59b7e0..4e43cfd11b083 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; +use Symfony\Component\Clock\MockClock; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; @@ -34,9 +35,12 @@ protected function tearDown(): void public static function getTimeZones() { - yield ['UTC']; - yield ['Etc/GMT+9']; - yield ['Etc/GMT-14']; + yield ['UTC', false]; + yield ['Etc/GMT+9', false]; + yield ['Etc/GMT-14', false]; + yield ['UTC', true]; + yield ['Etc/GMT+9', true]; + yield ['Etc/GMT-14', true]; } public static function getClasses() @@ -78,10 +82,10 @@ public function testUnsupportedArgument() /** * @dataProvider getTimeZones */ - public function testFullDate(string $timezone) + public function testFullDate(string $timezone, bool $withClock) { date_default_timezone_set($timezone); - $resolver = new DateTimeValueResolver(); + $resolver = new DateTimeValueResolver($withClock ? new MockClock() : null); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '2012-07-21 00:00:00']); @@ -97,10 +101,10 @@ public function testFullDate(string $timezone) /** * @dataProvider getTimeZones */ - public function testUnixTimestamp(string $timezone) + public function testUnixTimestamp(string $timezone, bool $withClock) { date_default_timezone_set($timezone); - $resolver = new DateTimeValueResolver(); + $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '989541720']); @@ -127,21 +131,46 @@ public function testNullableWithEmptyAttribute() } /** - * @dataProvider getTimeZones + * @param class-string<\DateTimeInterface> $class + * + * @dataProvider getClasses */ - public function testNow(string $timezone) + public function testNow(string $class) { - date_default_timezone_set($timezone); + date_default_timezone_set($timezone = 'Etc/GMT+9'); $resolver = new DateTimeValueResolver(); - $argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null, false); + $argument = new ArgumentMetadata('dummy', $class, false, false, null, false); $request = self::requestWithAttributes(['dummy' => null]); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); - $this->assertEquals('0', $results[0]->diff(new \DateTimeImmutable())->format('%s')); + $this->assertInstanceOf($class, $results[0]); $this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone'); + $this->assertEquals('0', $results[0]->diff(new \DateTimeImmutable())->format('%s')); + } + + /** + * @param class-string<\DateTimeInterface> $class + * + * @dataProvider getClasses + */ + public function testNowWithClock(string $class) + { + date_default_timezone_set('Etc/GMT+9'); + $clock = new MockClock('2022-02-20 22:20:02'); + $resolver = new DateTimeValueResolver($clock); + + $argument = new ArgumentMetadata('dummy', $class, false, false, null, false); + $request = self::requestWithAttributes(['dummy' => null]); + + $results = $resolver->resolve($request, $argument); + + $this->assertCount(1, $results); + $this->assertInstanceOf($class, $results[0]); + $this->assertSame('UTC', $results[0]->getTimezone()->getName(), 'Default timezone'); + $this->assertEquals($clock->now(), $results[0]); } /** @@ -181,10 +210,10 @@ public function testCustomClass() /** * @dataProvider getTimeZones */ - public function testDateTimeImmutable(string $timezone) + public function testDateTimeImmutable(string $timezone, bool $withClock) { date_default_timezone_set($timezone); - $resolver = new DateTimeValueResolver(); + $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '2016-09-08 00:00:00 +05:00']); @@ -200,10 +229,10 @@ public function testDateTimeImmutable(string $timezone) /** * @dataProvider getTimeZones */ - public function testWithFormat(string $timezone) + public function testWithFormat(string $timezone, bool $withClock) { date_default_timezone_set($timezone); - $resolver = new DateTimeValueResolver(); + $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeInterface::class, false, false, null, false, [ MapDateTime::class => new MapDateTime('m-d-y H:i:s'), diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 2c92557cbb6fb..41ef39d3ae749 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -26,6 +26,7 @@ }, "require-dev": { "symfony/browser-kit": "^5.4|^6.0", + "symfony/clock": "^6.2", "symfony/config": "^6.1", "symfony/console": "^5.4|^6.0", "symfony/css-selector": "^5.4|^6.0",
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: