From 92d01724686241218c20a696ae362d8a02d4a5f9 Mon Sep 17 00:00:00 2001 From: Shyim Date: Mon, 14 Oct 2024 15:41:45 +0200 Subject: [PATCH 01/19] [HttpKernel] Let Monolog create the log folder --- Kernel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kernel.php b/Kernel.php index 5f32158f68..223baa3afd 100644 --- a/Kernel.php +++ b/Kernel.php @@ -632,7 +632,7 @@ protected function getKernelParameters() */ protected function buildContainer() { - foreach (['cache' => $this->getCacheDir(), 'build' => $this->warmupDir ?: $this->getBuildDir(), 'logs' => $this->getLogDir()] as $name => $dir) { + foreach (['cache' => $this->getCacheDir(), 'build' => $this->warmupDir ?: $this->getBuildDir()] as $name => $dir) { if (!is_dir($dir)) { if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) { throw new \RuntimeException(sprintf('Unable to create the "%s" directory (%s).', $name, $dir)); From 4b68f96409285fc1fc370ed09d25dad449d78f03 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 20 Nov 2024 09:03:09 +0100 Subject: [PATCH 02/19] Bump version to 7.3 --- Kernel.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Kernel.php b/Kernel.php index 7e8b002079..ec5e3b0df3 100644 --- a/Kernel.php +++ b/Kernel.php @@ -73,15 +73,15 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.0-DEV'; - public const VERSION_ID = 70200; + public const VERSION = '7.3.0-DEV'; + public const VERSION_ID = 70300; public const MAJOR_VERSION = 7; - public const MINOR_VERSION = 2; + public const MINOR_VERSION = 3; public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; - public const END_OF_MAINTENANCE = '07/2025'; - public const END_OF_LIFE = '07/2025'; + public const END_OF_MAINTENANCE = '05/2025'; + public const END_OF_LIFE = '01/2026'; public function __construct( protected string $environment, From a1ecd5c472717cd4fb638b6c5f0ebbceb23096ce Mon Sep 17 00:00:00 2001 From: Felix Eymonot Date: Tue, 10 Dec 2024 12:40:59 +0100 Subject: [PATCH 03/19] [HttpKernel] [MapQueryString] added key argument to MapQueryString attribute --- Attribute/MapQueryString.php | 1 + CHANGELOG.md | 5 +++++ .../RequestPayloadValueResolver.php | 2 +- .../RequestPayloadValueResolverTest.php | 21 +++++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Attribute/MapQueryString.php b/Attribute/MapQueryString.php index dfff4ddcc9..07418df85c 100644 --- a/Attribute/MapQueryString.php +++ b/Attribute/MapQueryString.php @@ -37,6 +37,7 @@ public function __construct( public readonly string|GroupSequence|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_NOT_FOUND, + public readonly ?string $key = null, ) { parent::__construct($resolver); } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc103b48d..501ddbe6b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving + 7.2 --- diff --git a/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 1f0ff7cc0f..2da4b43905 100644 --- a/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -186,7 +186,7 @@ public static function getSubscribedEvents(): array private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object { - if (!($data = $request->query->all()) && ($argument->isNullable() || $argument->hasDefaultValue())) { + if (!($data = $request->query->all($attribute->key)) && ($argument->isNullable() || $argument->hasDefaultValue())) { return null; } diff --git a/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 8b26767f9e..2ed2e77042 100644 --- a/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -874,6 +874,27 @@ public function testBoolArgumentInJsonBody() $this->assertTrue($event->getArguments()[0]->value); } + + public function testConfigKeyForQueryString() + { + $serializer = new Serializer([new ObjectNormalizer()]); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('filtered', QueryPayload::class, false, false, null, false, [ + MapQueryString::class => new MapQueryString(key: 'value'), + ]); + $request = Request::create('/', Request::METHOD_GET, ['value' => ['page' => 1.0]]); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + $resolver->onKernelControllerArguments($event); + + $this->assertInstanceOf(QueryPayload::class, $event->getArguments()[0]); + $this->assertSame(1.0, $event->getArguments()[0]->page); + } } class RequestPayload From 406c453966dc1420d8b19ff45007bac8a51d401c Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 04/19] chore: PHP CS Fixer fixes --- DataCollector/ConfigDataCollector.php | 6 +++--- HttpCache/ResponseCacheStrategy.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DataCollector/ConfigDataCollector.php b/DataCollector/ConfigDataCollector.php index 8713dcf1e5..cc8ff3ada9 100644 --- a/DataCollector/ConfigDataCollector.php +++ b/DataCollector/ConfigDataCollector.php @@ -57,11 +57,11 @@ public function collect(Request $request, Response $response, ?\Throwable $excep 'php_intl_locale' => class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', 'php_timezone' => date_default_timezone_get(), 'xdebug_enabled' => \extension_loaded('xdebug'), - 'xdebug_status' => \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled (' . $xdebugMode . ')' : 'Not enabled') : 'Not installed', + 'xdebug_status' => \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled ('.$xdebugMode.')' : 'Not enabled') : 'Not installed', 'apcu_enabled' => \extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), - 'apcu_status' => \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', + 'apcu_status' => \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', 'zend_opcache_enabled' => \extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), - 'zend_opcache_status' => \extension_loaded('Zend OPcache') ? (filter_var(\ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', + 'zend_opcache_status' => \extension_loaded('Zend OPcache') ? (filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', 'bundles' => [], 'sapi_name' => \PHP_SAPI, ]; diff --git a/HttpCache/ResponseCacheStrategy.php b/HttpCache/ResponseCacheStrategy.php index 9176ba5881..4aba46728d 100644 --- a/HttpCache/ResponseCacheStrategy.php +++ b/HttpCache/ResponseCacheStrategy.php @@ -222,7 +222,7 @@ private function storeRelativeAgeDirective(string $directive, ?int $value, ?int } if (false !== $this->ageDirectives[$directive]) { - $value = min($value ?? PHP_INT_MAX, $expires ?? PHP_INT_MAX); + $value = min($value ?? \PHP_INT_MAX, $expires ?? \PHP_INT_MAX); $value -= $age; $this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value; } From 49bce5da5a3135fca24b33fda1f8276bdcd64f1b Mon Sep 17 00:00:00 2001 From: valtzu Date: Wed, 27 Nov 2024 22:28:12 +0200 Subject: [PATCH 05/19] Generate url-safe signatures --- Tests/Fragment/EsiFragmentRendererTest.php | 4 ++-- Tests/Fragment/HIncludeFragmentRendererTest.php | 2 +- Tests/Fragment/SsiFragmentRendererTest.php | 4 ++-- composer.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/Fragment/EsiFragmentRendererTest.php b/Tests/Fragment/EsiFragmentRendererTest.php index fa9885d275..6a08d7eae6 100644 --- a/Tests/Fragment/EsiFragmentRendererTest.php +++ b/Tests/Fragment/EsiFragmentRendererTest.php @@ -61,7 +61,7 @@ public function testRenderControllerReference() $altReference = new ControllerReference('alt_controller', [], []); $this->assertEquals( - '', + '', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } @@ -79,7 +79,7 @@ public function testRenderControllerReferenceWithAbsoluteUri() $altReference = new ControllerReference('alt_controller', [], []); $this->assertSame( - '', + '', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } diff --git a/Tests/Fragment/HIncludeFragmentRendererTest.php b/Tests/Fragment/HIncludeFragmentRendererTest.php index f74887ade3..82b80a86ff 100644 --- a/Tests/Fragment/HIncludeFragmentRendererTest.php +++ b/Tests/Fragment/HIncludeFragmentRendererTest.php @@ -32,7 +32,7 @@ public function testRenderWithControllerAndSigner() { $strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo')); - $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); + $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); } public function testRenderWithUri() diff --git a/Tests/Fragment/SsiFragmentRendererTest.php b/Tests/Fragment/SsiFragmentRendererTest.php index 4af00f9f75..0d3f1dc2d4 100644 --- a/Tests/Fragment/SsiFragmentRendererTest.php +++ b/Tests/Fragment/SsiFragmentRendererTest.php @@ -52,7 +52,7 @@ public function testRenderControllerReference() $altReference = new ControllerReference('alt_controller', [], []); $this->assertEquals( - '', + '', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } @@ -70,7 +70,7 @@ public function testRenderControllerReferenceWithAbsoluteUri() $altReference = new ControllerReference('alt_controller', [], []); $this->assertSame( - '', + '', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } diff --git a/composer.json b/composer.json index 89421417f5..e9cb077587 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-foundation": "^7.3", "symfony/polyfill-ctype": "^1.8", "psr/log": "^1|^2|^3" }, From f4a8c06e9a8b6e8a994ce4ecec9f7e334be933cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Thu, 26 Dec 2024 01:19:19 +0100 Subject: [PATCH 06/19] [Cache][HttpKernel] Add a `noStore` argument to the `#` attribute --- Attribute/Cache.php | 12 +++++ EventListener/CacheAttributeListener.php | 9 ++++ .../CacheAttributeListenerTest.php | 47 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/Attribute/Cache.php b/Attribute/Cache.php index 19d13e9228..fa2401a78c 100644 --- a/Attribute/Cache.php +++ b/Attribute/Cache.php @@ -102,6 +102,18 @@ public function __construct( * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). */ public int|string|null $staleIfError = null, + + /** + * Add the "no-store" Cache-Control directive when set to true. + * + * This directive indicates that no part of the response can be cached + * in any cache (not in a shared cache, nor in a private cache). + * + * Supersedes the "$public" and "$smaxage" values. + * + * @see https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.3 + */ + public ?bool $noStore = null, ) { } } diff --git a/EventListener/CacheAttributeListener.php b/EventListener/CacheAttributeListener.php index f428ea9462..e913edf9e5 100644 --- a/EventListener/CacheAttributeListener.php +++ b/EventListener/CacheAttributeListener.php @@ -163,6 +163,15 @@ public function onKernelResponse(ResponseEvent $event): void if (false === $cache->public) { $response->setPrivate(); } + + if (true === $cache->noStore) { + $response->setPrivate(); + $response->headers->addCacheControlDirective('no-store'); + } + + if (false === $cache->noStore) { + $response->headers->removeCacheControlDirective('no-store'); + } } } diff --git a/Tests/EventListener/CacheAttributeListenerTest.php b/Tests/EventListener/CacheAttributeListenerTest.php index b888579b80..b185ea8994 100644 --- a/Tests/EventListener/CacheAttributeListenerTest.php +++ b/Tests/EventListener/CacheAttributeListenerTest.php @@ -91,6 +91,50 @@ public function testResponseIsPrivateIfConfigurationIsPublicFalse() $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); } + public function testResponseIsPublicIfConfigurationIsPublicTrueNoStoreFalse() + { + $request = $this->createRequest(new Cache(public: true, noStore: false)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseIsPrivateIfConfigurationIsPublicTrueNoStoreTrue() + { + $request = $this->createRequest(new Cache(public: true, noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseIsPrivateNoStoreIfConfigurationIsNoStoreTrue() + { + $request = $this->createRequest(new Cache(noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseIsPrivateIfSharedMaxAgeSetAndNoStoreIsTrue() + { + $request = $this->createRequest(new Cache(smaxage: 1, noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + public function testResponseVary() { $vary = ['foobar']; @@ -132,6 +176,7 @@ public function testAttributeConfigurationsAreSetOnResponse() $this->assertFalse($this->response->headers->hasCacheControlDirective('max-stale')); $this->assertFalse($this->response->headers->hasCacheControlDirective('stale-while-revalidate')); $this->assertFalse($this->response->headers->hasCacheControlDirective('stale-if-error')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('no-store')); $this->request->attributes->set('_cache', [new Cache( expires: 'tomorrow', @@ -140,6 +185,7 @@ public function testAttributeConfigurationsAreSetOnResponse() maxStale: '5', staleWhileRevalidate: '6', staleIfError: '7', + noStore: true, )]); $this->listener->onKernelResponse($this->event); @@ -149,6 +195,7 @@ public function testAttributeConfigurationsAreSetOnResponse() $this->assertSame('5', $this->response->headers->getCacheControlDirective('max-stale')); $this->assertSame('6', $this->response->headers->getCacheControlDirective('stale-while-revalidate')); $this->assertSame('7', $this->response->headers->getCacheControlDirective('stale-if-error')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); $this->assertInstanceOf(\DateTimeInterface::class, $this->response->getExpires()); } From 9c1165cecf40349035aa5b8492caa4ccf5233973 Mon Sep 17 00:00:00 2001 From: Jan Rosier Date: Mon, 6 Jan 2025 15:35:18 +0100 Subject: [PATCH 07/19] Use spl_object_id() instead of spl_object_hash() --- DataCollector/LoggerDataCollector.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DataCollector/LoggerDataCollector.php b/DataCollector/LoggerDataCollector.php index 428d676240..29024f6e74 100644 --- a/DataCollector/LoggerDataCollector.php +++ b/DataCollector/LoggerDataCollector.php @@ -233,10 +233,10 @@ private function sanitizeLogs(array $logs): array $exception = $log['context']['exception']; if ($exception instanceof SilencedErrorContext) { - if (isset($silencedLogs[$h = spl_object_hash($exception)])) { + if (isset($silencedLogs[$id = spl_object_id($exception)])) { continue; } - $silencedLogs[$h] = true; + $silencedLogs[$id] = true; if (!isset($sanitizedLogs[$message])) { $sanitizedLogs[$message] = $log + [ @@ -312,10 +312,10 @@ private function computeErrorsCount(array $containerDeprecationLogs): array if ($this->isSilencedOrDeprecationErrorLog($log)) { $exception = $log['context']['exception']; if ($exception instanceof SilencedErrorContext) { - if (isset($silencedLogs[$h = spl_object_hash($exception)])) { + if (isset($silencedLogs[$id = spl_object_id($exception)])) { continue; } - $silencedLogs[$h] = true; + $silencedLogs[$id] = true; $count['scream_count'] += $exception->count; } else { ++$count['deprecation_count']; From eb0e7ae3cffc41d41fc9705c35366e8aac763d95 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 10 Jan 2025 15:17:09 +0100 Subject: [PATCH 08/19] chore: PHP CS Fixer fixes --- .../ArgumentResolver/RequestPayloadValueResolverTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 649a7dc87e..77cf7d9c58 100644 --- a/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -420,7 +420,7 @@ public function testQueryStringParameterTypeMismatch() try { $resolver->onKernelControllerArguments($event); - $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); @@ -514,7 +514,7 @@ public function testRequestInputTypeMismatch() try { $resolver->onKernelControllerArguments($event); - $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); From dc54eb349f6b261db71cb6d5bddf6ca4241deef9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 15 Jan 2025 18:41:27 +0100 Subject: [PATCH 09/19] [HttpKernel] Improve MapQueryParameter handling of options --- Attribute/MapQueryParameter.php | 6 ++--- .../QueryParameterValueResolver.php | 2 +- .../QueryParameterValueResolverTest.php | 22 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Attribute/MapQueryParameter.php b/Attribute/MapQueryParameter.php index ec9bf57726..486813a820 100644 --- a/Attribute/MapQueryParameter.php +++ b/Attribute/MapQueryParameter.php @@ -28,9 +28,9 @@ final class MapQueryParameter extends ValueResolver * @see https://php.net/manual/filter.constants for filter, flags and options * * @param string|null $name The name of the query parameter; if null, the name of the argument in the controller will be used - * @param (FILTER_VALIDATE_*)|(FILTER_SANITIZE_*)|null $filter The filter to pass to "filter_var()" - * @param int-mask-of<(FILTER_FLAG_*)|FILTER_NULL_ON_FAILURE> $flags The flags to pass to "filter_var()" - * @param array $options The options to pass to "filter_var()" + * @param (FILTER_VALIDATE_*)|(FILTER_SANITIZE_*)|null $filter The filter to pass to "filter_var()", deduced from the type-hint if null + * @param int-mask-of<(FILTER_FLAG_*)|FILTER_NULL_ON_FAILURE> $flags + * @param array{min_range?: int|float, max_range?: int|float, regexp?: string, ...} $options * @param class-string|string $resolver The name of the resolver to use */ public function __construct( diff --git a/Controller/ArgumentResolver/QueryParameterValueResolver.php b/Controller/ArgumentResolver/QueryParameterValueResolver.php index 8ceaba6da4..1566119dfd 100644 --- a/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -76,7 +76,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $enumType = null; $filter = match ($type) { 'array' => \FILTER_DEFAULT, - 'string' => \FILTER_DEFAULT, + 'string' => isset($attribute->options['regexp']) ? \FILTER_VALIDATE_REGEXP : \FILTER_DEFAULT, 'int' => \FILTER_VALIDATE_INT, 'float' => \FILTER_VALIDATE_FLOAT, 'bool' => \FILTER_VALIDATE_BOOL, diff --git a/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php b/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php index 43161d1a10..194cfd2e52 100644 --- a/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php @@ -108,50 +108,50 @@ public static function validDataProvider(): iterable yield 'parameter found and string with regexp filter that matches' => [ Request::create('/', 'GET', ['firstName' => 'John']), - new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), + new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(options: ['regexp' => '/John/'])]), ['John'], ]; yield 'parameter found and string with regexp filter that falls back to null on failure' => [ Request::create('/', 'GET', ['firstName' => 'Fabien']), - new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), + new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), [null], ]; yield 'parameter found and string variadic with regexp filter that matches' => [ Request::create('/', 'GET', ['firstName' => ['John', 'John']]), - new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), + new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(options: ['regexp' => '/John/'])]), ['John', 'John'], ]; yield 'parameter found and string variadic with regexp filter that falls back to null on failure' => [ Request::create('/', 'GET', ['firstName' => ['John', 'Fabien']]), - new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), + new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), ['John'], ]; yield 'parameter found and integer' => [ - Request::create('/', 'GET', ['age' => 123]), + Request::create('/', 'GET', ['age' => '123']), new ArgumentMetadata('age', 'int', false, false, false, attributes: [new MapQueryParameter()]), [123], ]; yield 'parameter found and integer variadic' => [ - Request::create('/', 'GET', ['age' => [123, 222]]), + Request::create('/', 'GET', ['age' => ['123', '222']]), new ArgumentMetadata('age', 'int', true, false, false, attributes: [new MapQueryParameter()]), [123, 222], ]; yield 'parameter found and float' => [ - Request::create('/', 'GET', ['price' => 10.99]), + Request::create('/', 'GET', ['price' => '10.99']), new ArgumentMetadata('price', 'float', false, false, false, attributes: [new MapQueryParameter()]), [10.99], ]; yield 'parameter found and float variadic' => [ - Request::create('/', 'GET', ['price' => [10.99, 5.99]]), + Request::create('/', 'GET', ['price' => ['10.99e2', '5.99']]), new ArgumentMetadata('price', 'float', true, false, false, attributes: [new MapQueryParameter()]), - [10.99, 5.99], + [1099.0, 5.99], ]; yield 'parameter found and boolean yes' => [ @@ -209,7 +209,7 @@ public static function validDataProvider(): iterable ]; yield 'parameter found and backing type variadic and at least one backing value not int nor string that fallbacks to null on failure' => [ - Request::create('/', 'GET', ['suits' => [1, 'D']]), + Request::create('/', 'GET', ['suits' => ['1', 'D']]), new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]), [null], ]; @@ -265,7 +265,7 @@ public static function invalidArgumentTypeProvider(): iterable public static function invalidOrMissingArgumentProvider(): iterable { yield 'parameter found and array variadic with parameter not array failure' => [ - Request::create('/', 'GET', ['ids' => [['1', '2'], 1]]), + Request::create('/', 'GET', ['ids' => [['1', '2'], '1']]), new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]), new NotFoundHttpException('Invalid query parameter "ids".'), ]; From cb1eeade523fd4daae0b053678bca1361b4465e3 Mon Sep 17 00:00:00 2001 From: seb-jean Date: Wed, 30 Oct 2024 14:27:35 +0100 Subject: [PATCH 10/19] [HttpKernel] Support `Uid` in `#[MapQueryParameter]` --- CHANGELOG.md | 1 + .../QueryParameterValueResolver.php | 14 +++++++++++++- .../QueryParameterValueResolverTest.php | 13 ++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 501ddbe6b7..cc9da8a2a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving + * Support `Uid` in `#[MapQueryParameter]` 7.2 --- diff --git a/Controller/ArgumentResolver/QueryParameterValueResolver.php b/Controller/ArgumentResolver/QueryParameterValueResolver.php index 1566119dfd..5fe3d75313 100644 --- a/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\Uid\AbstractUid; /** * Resolve arguments of type: array, string, int, float, bool, \BackedEnum from query parameters. @@ -73,6 +74,12 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $options['flags'] |= \FILTER_REQUIRE_SCALAR; } + $uidType = null; + if (is_subclass_of($type, AbstractUid::class)) { + $uidType = $type; + $type = 'uid'; + } + $enumType = null; $filter = match ($type) { 'array' => \FILTER_DEFAULT, @@ -80,10 +87,11 @@ public function resolve(Request $request, ArgumentMetadata $argument): array 'int' => \FILTER_VALIDATE_INT, 'float' => \FILTER_VALIDATE_FLOAT, 'bool' => \FILTER_VALIDATE_BOOL, + 'uid' => \FILTER_DEFAULT, default => match ($enumType = is_subclass_of($type, \BackedEnum::class) ? (new \ReflectionEnum($type))->getBackingType()->getName() : null) { 'int' => \FILTER_VALIDATE_INT, 'string' => \FILTER_DEFAULT, - default => throw new \LogicException(\sprintf('#[MapQueryParameter] cannot be used on controller argument "%s$%s" of type "%s"; one of array, string, int, float, bool or \BackedEnum should be used.', $argument->isVariadic() ? '...' : '', $argument->getName(), $type ?? 'mixed')), + default => throw new \LogicException(\sprintf('#[MapQueryParameter] cannot be used on controller argument "%s$%s" of type "%s"; one of array, string, int, float, bool, uid or \BackedEnum should be used.', $argument->isVariadic() ? '...' : '', $argument->getName(), $type ?? 'mixed')), }, }; @@ -105,6 +113,10 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $value = \is_array($value) ? array_map($enumFrom, $value) : $enumFrom($value); } + if (null !== $uidType) { + $value = \is_array($value) ? array_map([$uidType, 'fromString'], $value) : $uidType::fromString($value); + } + if (null === $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } diff --git a/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php b/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php index 194cfd2e52..2b887db821 100644 --- a/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; +use Symfony\Component\Uid\Ulid; class QueryParameterValueResolverTest extends TestCase { @@ -44,7 +45,7 @@ public function testSkipWhenNoAttribute() */ public function testResolvingSuccessfully(Request $request, ArgumentMetadata $metadata, array $expected) { - $this->assertSame($expected, $this->resolver->resolve($request, $metadata)); + $this->assertEquals($expected, $this->resolver->resolve($request, $metadata)); } /** @@ -231,6 +232,12 @@ public static function validDataProvider(): iterable new ArgumentMetadata('firstName', 'string', false, true, false, attributes: [new MapQueryParameter()]), [], ]; + + yield 'parameter found and ULID' => [ + Request::create('/', 'GET', ['groupId' => '01E439TP9XJZ9RPFH3T1PYBCR8']), + new ArgumentMetadata('groupId', Ulid::class, false, true, false, attributes: [new MapQueryParameter()]), + [Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8')], + ]; } /** @@ -245,13 +252,13 @@ public static function invalidArgumentTypeProvider(): iterable yield 'unsupported type' => [ Request::create('/', 'GET', ['standardClass' => 'test']), new ArgumentMetadata('standardClass', \stdClass::class, false, false, false, attributes: [new MapQueryParameter()]), - '#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.', + '#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float, bool, uid or \BackedEnum should be used.', ]; yield 'unsupported type variadic' => [ Request::create('/', 'GET', ['standardClass' => 'test']), new ArgumentMetadata('standardClass', \stdClass::class, true, false, false, attributes: [new MapQueryParameter()]), - '#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float, bool or \BackedEnum should be used.', + '#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float, bool, uid or \BackedEnum should be used.', ]; } From 6feb9cf0d7ef1b7e01dadfd6c9ee5925eff57b7d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Mar 2025 16:03:52 +0100 Subject: [PATCH 11/19] replace assertEmpty() with stricter assertions --- .../TraceableValueResolverTest.php | 2 +- .../UploadedFileValueResolverTest.php | 2 +- Tests/DataCollector/DumpDataCollectorTest.php | 6 ++--- .../AddAnnotatedClassesToCachePassTest.php | 22 +++++++++---------- ...sterControllerArgumentLocatorsPassTest.php | 6 ++--- Tests/EventListener/SessionListenerTest.php | 8 +++---- Tests/Fragment/InlineFragmentRendererTest.php | 2 +- Tests/HttpCache/HttpCacheTest.php | 12 +++++----- Tests/HttpCache/StoreTest.php | 10 ++++----- Tests/Profiler/FileProfilerStorageTest.php | 2 +- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php b/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php index 5ede33ccb3..cf4e837f73 100644 --- a/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php @@ -32,7 +32,7 @@ public function testTimingsInResolve() foreach ($iterable as $index => $resolved) { $event = $stopwatch->getEvent(ResolverStub::class.'::resolve'); $this->assertTrue($event->isStarted()); - $this->assertEmpty($event->getPeriods()); + $this->assertSame([], $event->getPeriods()); switch ($index) { case 0: $this->assertEquals('first', $resolved); diff --git a/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php b/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php index 5eb0d32483..11ab6f36a1 100644 --- a/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php @@ -85,7 +85,7 @@ static function () {}, $resolver->onKernelControllerArguments($event); $data = $event->getArguments()[0]; - $this->assertEmpty($data); + $this->assertSame([], $data); } /** diff --git a/Tests/DataCollector/DumpDataCollectorTest.php b/Tests/DataCollector/DumpDataCollectorTest.php index e55af09fe5..11e1bc2e6c 100644 --- a/Tests/DataCollector/DumpDataCollectorTest.php +++ b/Tests/DataCollector/DumpDataCollectorTest.php @@ -79,7 +79,7 @@ public function testDumpWithServerConnection() // Collect doesn't re-trigger dump ob_start(); $collector->collect(new Request(), new Response()); - $this->assertEmpty(ob_get_clean()); + $this->assertSame('', ob_get_clean()); $this->assertStringMatchesFormat('%a;a:%d:{i:0;a:6:{s:4:"data";%c:39:"Symfony\Component\VarDumper\Cloner\Data":%a', serialize($collector)); } @@ -157,7 +157,7 @@ public function testFlushNothingWhenDataDumperIsProvided() ob_start(); $collector->__destruct(); - $this->assertEmpty(ob_get_clean()); + $this->assertSame('', ob_get_clean()); } public function testNullContentTypeWithNoDebugEnv() @@ -175,6 +175,6 @@ public function testNullContentTypeWithNoDebugEnv() ob_start(); $collector->__destruct(); - $this->assertEmpty(ob_get_clean()); + $this->assertSame('', ob_get_clean()); } } diff --git a/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php b/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php index 387a5108ec..e57c349609 100644 --- a/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php +++ b/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php @@ -36,33 +36,33 @@ public function testExpandClasses() $this->assertSame('Foo\\Bar', $expand(['Foo\\'], ['\\Foo\\Bar'])[0]); $this->assertSame('Foo\\Bar\\Acme', $expand(['Foo\\'], ['\\Foo\\Bar\\Acme'])[0]); - $this->assertEmpty($expand(['Foo\\'], ['\\Foo'])); + $this->assertSame([], $expand(['Foo\\'], ['\\Foo'])); $this->assertSame('Acme\\Foo\\Bar', $expand(['**\\Foo\\'], ['\\Acme\\Foo\\Bar'])[0]); - $this->assertEmpty($expand(['**\\Foo\\'], ['\\Foo\\Bar'])); - $this->assertEmpty($expand(['**\\Foo\\'], ['\\Acme\\Foo'])); - $this->assertEmpty($expand(['**\\Foo\\'], ['\\Foo'])); + $this->assertSame([], $expand(['**\\Foo\\'], ['\\Foo\\Bar'])); + $this->assertSame([], $expand(['**\\Foo\\'], ['\\Acme\\Foo'])); + $this->assertSame([], $expand(['**\\Foo\\'], ['\\Foo'])); $this->assertSame('Acme\\Foo', $expand(['**\\Foo'], ['\\Acme\\Foo'])[0]); - $this->assertEmpty($expand(['**\\Foo'], ['\\Acme\\Foo\\AcmeBundle'])); - $this->assertEmpty($expand(['**\\Foo'], ['\\Acme\\FooBar\\AcmeBundle'])); + $this->assertSame([], $expand(['**\\Foo'], ['\\Acme\\Foo\\AcmeBundle'])); + $this->assertSame([], $expand(['**\\Foo'], ['\\Acme\\FooBar\\AcmeBundle'])); $this->assertSame('Foo\\Acme\\Bar', $expand(['Foo\\*\\Bar'], ['\\Foo\\Acme\\Bar'])[0]); - $this->assertEmpty($expand(['Foo\\*\\Bar'], ['\\Foo\\Acme\\Bundle\\Bar'])); + $this->assertSame([], $expand(['Foo\\*\\Bar'], ['\\Foo\\Acme\\Bundle\\Bar'])); $this->assertSame('Foo\\Acme\\Bar', $expand(['Foo\\**\\Bar'], ['\\Foo\\Acme\\Bar'])[0]); $this->assertSame('Foo\\Acme\\Bundle\\Bar', $expand(['Foo\\**\\Bar'], ['\\Foo\\Acme\\Bundle\\Bar'])[0]); $this->assertSame('Acme\\Bar', $expand(['*\\Bar'], ['\\Acme\\Bar'])[0]); - $this->assertEmpty($expand(['*\\Bar'], ['\\Bar'])); - $this->assertEmpty($expand(['*\\Bar'], ['\\Foo\\Acme\\Bar'])); + $this->assertSame([], $expand(['*\\Bar'], ['\\Bar'])); + $this->assertSame([], $expand(['*\\Bar'], ['\\Foo\\Acme\\Bar'])); $this->assertSame('Foo\\Acme\\Bar', $expand(['**\\Bar'], ['\\Foo\\Acme\\Bar'])[0]); $this->assertSame('Foo\\Acme\\Bundle\\Bar', $expand(['**\\Bar'], ['\\Foo\\Acme\\Bundle\\Bar'])[0]); - $this->assertEmpty($expand(['**\\Bar'], ['\\Bar'])); + $this->assertSame([], $expand(['**\\Bar'], ['\\Bar'])); $this->assertSame('Foo\\Bar', $expand(['Foo\\*'], ['\\Foo\\Bar'])[0]); - $this->assertEmpty($expand(['Foo\\*'], ['\\Foo\\Acme\\Bar'])); + $this->assertSame([], $expand(['Foo\\*'], ['\\Foo\\Acme\\Bar'])); $this->assertSame('Foo\\Bar', $expand(['Foo\\**'], ['\\Foo\\Bar'])[0]); $this->assertSame('Foo\\Acme\\Bar', $expand(['Foo\\**'], ['\\Foo\\Acme\\Bar'])[0]); diff --git a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 02b5df6512..03aef073d0 100644 --- a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -277,7 +277,7 @@ public function testArgumentWithNoTypeHintIsOk() $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $this->assertEmpty(array_keys($locator)); + $this->assertSame([], array_keys($locator)); } public function testControllersAreMadePublic() @@ -440,7 +440,7 @@ public function testEnumArgumentIsIgnored() $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $this->assertEmpty(array_keys($locator), 'enum typed argument is ignored'); + $this->assertSame([], array_keys($locator), 'enum typed argument is ignored'); } public function testBindWithTarget() @@ -480,7 +480,7 @@ public function testResponseArgumentIsIgnored() (new RegisterControllerArgumentLocatorsPass())->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $this->assertEmpty(array_keys($locator), 'Response typed argument is ignored'); + $this->assertSame([], array_keys($locator), 'Response typed argument is ignored'); } public function testAutowireAttribute() diff --git a/Tests/EventListener/SessionListenerTest.php b/Tests/EventListener/SessionListenerTest.php index 2aa5d622e2..deba6661f0 100644 --- a/Tests/EventListener/SessionListenerTest.php +++ b/Tests/EventListener/SessionListenerTest.php @@ -896,8 +896,8 @@ public function testReset() (new SessionListener($container, true))->reset(); - $this->assertEmpty($_SESSION); - $this->assertEmpty(session_id()); + $this->assertSame([], $_SESSION); + $this->assertSame('', session_id()); $this->assertSame(\PHP_SESSION_NONE, session_status()); } @@ -917,8 +917,8 @@ public function testResetUnclosedSession() (new SessionListener($container, true))->reset(); - $this->assertEmpty($_SESSION); - $this->assertEmpty(session_id()); + $this->assertSame([], $_SESSION); + $this->assertSame('', session_id()); $this->assertSame(\PHP_SESSION_NONE, session_status()); } diff --git a/Tests/Fragment/InlineFragmentRendererTest.php b/Tests/Fragment/InlineFragmentRendererTest.php index 2d492c5359..8266458fd6 100644 --- a/Tests/Fragment/InlineFragmentRendererTest.php +++ b/Tests/Fragment/InlineFragmentRendererTest.php @@ -97,7 +97,7 @@ public function testRenderExceptionIgnoreErrors() $strategy = new InlineFragmentRenderer($kernel, $dispatcher); - $this->assertEmpty($strategy->render('/', $request, ['ignore_errors' => true])->getContent()); + $this->assertSame('', $strategy->render('/', $request, ['ignore_errors' => true])->getContent()); } public function testRenderExceptionIgnoreErrorsWithAlt() diff --git a/Tests/HttpCache/HttpCacheTest.php b/Tests/HttpCache/HttpCacheTest.php index 0a9e548990..e82e8fd81b 100644 --- a/Tests/HttpCache/HttpCacheTest.php +++ b/Tests/HttpCache/HttpCacheTest.php @@ -195,7 +195,7 @@ public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified() $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); $this->assertEquals('', $this->response->headers->get('Content-Type')); - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); } @@ -209,7 +209,7 @@ public function testRespondsWith304WhenIfNoneMatchMatchesETag() $this->assertEquals(304, $this->response->getStatusCode()); $this->assertEquals('', $this->response->headers->get('Content-Type')); $this->assertTrue($this->response->headers->has('ETag')); - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); } @@ -1281,7 +1281,7 @@ public function testEsiCacheSendsTheLowestTtlForHeadRequests() $this->request('HEAD', '/', [], [], true); - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertEquals(100, $this->response->getTtl()); } @@ -1510,7 +1510,7 @@ public function testEsiCacheForceValidationForHeadRequests() // The response has been assembled from expiration and validation based resources // This can neither be cached nor revalidated, so it should be private/no cache - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertNull($this->response->getTtl()); $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache')); @@ -1568,7 +1568,7 @@ public function testEsiRecalculateContentLengthHeaderForHeadRequest() // in decimal number of OCTETs, sent to the recipient or, in the case of the HEAD // method, the size of the entity-body that would have been sent had the request // been a GET." - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertEquals(12, $this->response->headers->get('Content-Length')); } @@ -1692,7 +1692,7 @@ public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadReq $this->setNextResponses($responses); $this->request('HEAD', '/', [], [], true); - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertNull($this->response->getETag()); $this->assertNull($this->response->getLastModified()); } diff --git a/Tests/HttpCache/StoreTest.php b/Tests/HttpCache/StoreTest.php index 8c41ac5986..a24aa95c87 100644 --- a/Tests/HttpCache/StoreTest.php +++ b/Tests/HttpCache/StoreTest.php @@ -40,7 +40,7 @@ protected function tearDown(): void public function testReadsAnEmptyArrayWithReadWhenNothingCachedAtKey() { - $this->assertEmpty($this->getStoreMetadata('/nothing')); + $this->assertSame([], $this->getStoreMetadata('/nothing')); } public function testUnlockFileThatDoesExist() @@ -65,7 +65,7 @@ public function testRemovesEntriesForKeyWithPurge() $this->assertNotEmpty($metadata); $this->assertTrue($this->store->purge('/foo')); - $this->assertEmpty($this->getStoreMetadata($request)); + $this->assertSame([], $this->getStoreMetadata($request)); // cached content should be kept after purging $path = $this->store->getPath($metadata[0][1]['x-content-digest'][0]); @@ -291,7 +291,7 @@ public function testPurgeHttps() $this->assertNotEmpty($this->getStoreMetadata($request)); $this->assertTrue($this->store->purge('https://example.com/foo')); - $this->assertEmpty($this->getStoreMetadata($request)); + $this->assertSame([], $this->getStoreMetadata($request)); } public function testPurgeHttpAndHttps() @@ -306,8 +306,8 @@ public function testPurgeHttpAndHttps() $this->assertNotEmpty($this->getStoreMetadata($requestHttps)); $this->assertTrue($this->store->purge('http://example.com/foo')); - $this->assertEmpty($this->getStoreMetadata($requestHttp)); - $this->assertEmpty($this->getStoreMetadata($requestHttps)); + $this->assertSame([], $this->getStoreMetadata($requestHttp)); + $this->assertSame([], $this->getStoreMetadata($requestHttps)); } public function testDoesNotStorePrivateHeaders() diff --git a/Tests/Profiler/FileProfilerStorageTest.php b/Tests/Profiler/FileProfilerStorageTest.php index d191ff074e..eb8f99c806 100644 --- a/Tests/Profiler/FileProfilerStorageTest.php +++ b/Tests/Profiler/FileProfilerStorageTest.php @@ -292,7 +292,7 @@ public function testPurge() $this->storage->purge(); - $this->assertEmpty($this->storage->read('token'), '->purge() removes all data stored by profiler'); + $this->assertNull($this->storage->read('token'), '->purge() removes all data stored by profiler'); $this->assertCount(0, $this->storage->find('127.0.0.1', '', 10, 'GET'), '->purge() removes all items from index'); } From 1b5ec906db2d2feebc3c7846a2ce549854314a60 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 15 Mar 2025 14:10:48 +0100 Subject: [PATCH 12/19] Various cleanups --- DependencyInjection/ServicesResetter.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DependencyInjection/ServicesResetter.php b/DependencyInjection/ServicesResetter.php index 7636e52a18..c2a19d992f 100644 --- a/DependencyInjection/ServicesResetter.php +++ b/DependencyInjection/ServicesResetter.php @@ -46,6 +46,10 @@ public function reset(): void continue; } + if (\PHP_VERSION_ID >= 80400 && (new \ReflectionClass($service))->isUninitializedLazyObject($service)) { + continue; + } + foreach ((array) $this->resetMethods[$id] as $resetMethod) { if ('?' === $resetMethod[0] && !method_exists($service, $resetMethod = substr($resetMethod, 1))) { continue; From 2ec3b84cd88992214f379059b03625ac1efbc90c Mon Sep 17 00:00:00 2001 From: Pierre Ambroise Date: Wed, 19 Mar 2025 09:22:52 +0100 Subject: [PATCH 13/19] Open doc in new page in default page --- Resources/welcome.html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/welcome.html.php b/Resources/welcome.html.php index 810c71f988..549c4ff192 100644 --- a/Resources/welcome.html.php +++ b/Resources/welcome.html.php @@ -265,7 +265,7 @@ Next Step - Create your first page + Create your first page to replace this placeholder page.

From 84b25e32c3872d70c92fcb848b35800df2c8052d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 21 Mar 2025 13:14:10 +0100 Subject: [PATCH 14/19] [FrameworkBundle] Add alias `ServicesResetter` for `services_resetter` service --- CHANGELOG.md | 1 + DependencyInjection/ServicesResetter.php | 3 +-- .../ServicesResetterInterface.php | 21 +++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 DependencyInjection/ServicesResetterInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9da8a2a9..1d533c29f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving * Support `Uid` in `#[MapQueryParameter]` + * Add `ServicesResetterInterface`, implemented by `ServicesResetter` 7.2 --- diff --git a/DependencyInjection/ServicesResetter.php b/DependencyInjection/ServicesResetter.php index c2a19d992f..57e394fcc5 100644 --- a/DependencyInjection/ServicesResetter.php +++ b/DependencyInjection/ServicesResetter.php @@ -13,7 +13,6 @@ use ProxyManager\Proxy\LazyLoadingInterface; use Symfony\Component\VarExporter\LazyObjectInterface; -use Symfony\Contracts\Service\ResetInterface; /** * Resets provided services. @@ -23,7 +22,7 @@ * * @final since Symfony 7.2 */ -class ServicesResetter implements ResetInterface +class ServicesResetter implements ServicesResetterInterface { /** * @param \Traversable $resettableServices diff --git a/DependencyInjection/ServicesResetterInterface.php b/DependencyInjection/ServicesResetterInterface.php new file mode 100644 index 0000000000..88fba821db --- /dev/null +++ b/DependencyInjection/ServicesResetterInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\DependencyInjection; + +use Symfony\Contracts\Service\ResetInterface; + +/** + * Resets provided services. + */ +interface ServicesResetterInterface extends ResetInterface +{ +} From d71f6c83a7d35a72397ab7fbdfdc5aad22635de7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 28 Feb 2025 22:27:11 +0100 Subject: [PATCH 15/19] [VarExporter] Leverage native lazy objects --- Tests/Fixtures/LazyResettableService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Fixtures/LazyResettableService.php b/Tests/Fixtures/LazyResettableService.php index 543cf0d953..1b66415c4b 100644 --- a/Tests/Fixtures/LazyResettableService.php +++ b/Tests/Fixtures/LazyResettableService.php @@ -11,7 +11,7 @@ namespace Symfony\Component\HttpKernel\Tests\Fixtures; -class LazyResettableService +class LazyResettableService extends \stdClass { public static $counter = 0; From aa58e4994cea483bd1c2166817ae9b11c77236a1 Mon Sep 17 00:00:00 2001 From: eltharin Date: Mon, 3 Mar 2025 21:12:56 +0100 Subject: [PATCH 16/19] [Routing] Add alias in `{foo:bar}` syntax in route parameter --- EventListener/RouterListener.php | 9 ++++- Tests/EventListener/RouterListenerTest.php | 44 ++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/EventListener/RouterListener.php b/EventListener/RouterListener.php index fb4bd60bc3..dd6f5bb214 100644 --- a/EventListener/RouterListener.php +++ b/EventListener/RouterListener.php @@ -117,7 +117,14 @@ public function onKernelRequest(RequestEvent $event): void $attributes = []; foreach ($parameters as $parameter => $value) { - $attribute = $mapping[$parameter] ?? $parameter; + if (!isset($mapping[$parameter])) { + $attribute = $parameter; + } elseif (\is_array($mapping[$parameter])) { + [$attribute, $parameter] = $mapping[$parameter]; + $mappedAttributes[$attribute] = ''; + } else { + $attribute = $mapping[$parameter]; + } if (!isset($mappedAttributes[$attribute])) { $attributes[$attribute] = $value; diff --git a/Tests/EventListener/RouterListenerTest.php b/Tests/EventListener/RouterListenerTest.php index d13093db0c..ca7bb1b1f6 100644 --- a/Tests/EventListener/RouterListenerTest.php +++ b/Tests/EventListener/RouterListenerTest.php @@ -323,5 +323,49 @@ public static function provideRouteMapping(): iterable ], ], ]; + + yield [ + [ + 'conference' => ['slug' => 'vienna-2024'], + ], + [ + 'slug' => 'vienna-2024', + '_route_mapping' => [ + 'slug' => [ + 'conference', + 'slug', + ], + ], + ], + ]; + + yield [ + [ + 'article' => [ + 'id' => 'abc123', + 'date' => '2024-04-24', + 'slug' => 'symfony-rocks', + ], + ], + [ + 'id' => 'abc123', + 'date' => '2024-04-24', + 'slug' => 'symfony-rocks', + '_route_mapping' => [ + 'id' => [ + 'article', + 'id' + ], + 'date' => [ + 'article', + 'date', + ], + 'slug' => [ + 'article', + 'slug', + ], + ], + ], + ]; } } From 5e575fbdc09d69e68bb8fd8dd4bde2eab5737c86 Mon Sep 17 00:00:00 2001 From: Arkalo2 <24898676+Arkalo2@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:57:41 +0100 Subject: [PATCH 17/19] [FrameworkBundle][HttpKernel] Allow configuring the logging channel per type of exceptions --- CHANGELOG.md | 3 +- EventListener/ErrorListener.php | 38 +++++++++++---- Tests/EventListener/ErrorListenerTest.php | 57 +++++++++++++++++++++++ 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d533c29f5..6bf1a60ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ CHANGELOG * Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving * Support `Uid` in `#[MapQueryParameter]` * Add `ServicesResetterInterface`, implemented by `ServicesResetter` - + * Allow configuring the logging channel per type of exceptions in ErrorListener + 7.2 --- diff --git a/EventListener/ErrorListener.php b/EventListener/ErrorListener.php index c677958cde..18e8bff441 100644 --- a/EventListener/ErrorListener.php +++ b/EventListener/ErrorListener.php @@ -34,13 +34,14 @@ class ErrorListener implements EventSubscriberInterface { /** - * @param array|null}> $exceptionsMapping + * @param array|null, log_channel: string|null}> $exceptionsMapping */ public function __construct( protected string|object|array|null $controller, protected ?LoggerInterface $logger = null, protected bool $debug = false, protected array $exceptionsMapping = [], + protected array $loggers = [], ) { } @@ -48,6 +49,7 @@ public function logKernelException(ExceptionEvent $event): void { $throwable = $event->getThrowable(); $logLevel = $this->resolveLogLevel($throwable); + $logChannel = $this->resolveLogChannel($throwable); foreach ($this->exceptionsMapping as $class => $config) { if (!$throwable instanceof $class || !$config['status_code']) { @@ -69,7 +71,7 @@ public function logKernelException(ExceptionEvent $event): void $e = FlattenException::createFromThrowable($throwable); - $this->logException($throwable, \sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), basename($e->getFile()), $e->getLine()), $logLevel); + $this->logException($throwable, \sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), basename($e->getFile()), $e->getLine()), $logLevel, $logChannel); } public function onKernelException(ExceptionEvent $event): void @@ -159,16 +161,20 @@ public static function getSubscribedEvents(): array /** * Logs an exception. + * + * @param ?string $logChannel */ - protected function logException(\Throwable $exception, string $message, ?string $logLevel = null): void + protected function logException(\Throwable $exception, string $message, ?string $logLevel = null, /* ?string $logChannel = null */): void { - if (null === $this->logger) { + $logChannel = (3 < \func_num_args() ? \func_get_arg(3) : null) ?? $this->resolveLogChannel($exception); + + $logLevel ??= $this->resolveLogLevel($exception); + + if(!$logger = $this->getLogger($logChannel)) { return; } - $logLevel ??= $this->resolveLogLevel($exception); - - $this->logger->log($logLevel, $message, ['exception' => $exception]); + $logger->log($logLevel, $message, ['exception' => $exception]); } /** @@ -193,6 +199,17 @@ private function resolveLogLevel(\Throwable $throwable): string return LogLevel::ERROR; } + private function resolveLogChannel(\Throwable $throwable): ?string + { + foreach ($this->exceptionsMapping as $class => $config) { + if ($throwable instanceof $class && isset($config['log_channel'])) { + return $config['log_channel']; + } + } + + return null; + } + /** * Clones the request for the exception. */ @@ -201,7 +218,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re $attributes = [ '_controller' => $this->controller, 'exception' => $exception, - 'logger' => DebugLoggerConfigurator::getDebugLogger($this->logger), + 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($exception)), ]; $request = $request->duplicate(null, null, $attributes); $request->setMethod('GET'); @@ -249,4 +266,9 @@ private function getInheritedAttribute(string $class, string $attribute): ?objec return $attributeReflector?->newInstance(); } + + private function getLogger(?string $logChannel): ?LoggerInterface + { + return $logChannel ? $this->loggers[$logChannel] ?? $this->logger : $this->logger; + } } diff --git a/Tests/EventListener/ErrorListenerTest.php b/Tests/EventListener/ErrorListenerTest.php index 2e1f7d58b7..7fdda59635 100644 --- a/Tests/EventListener/ErrorListenerTest.php +++ b/Tests/EventListener/ErrorListenerTest.php @@ -143,6 +143,63 @@ public function testHandleWithLogLevelAttribute() $this->assertCount(1, $logger->getLogsForLevel('warning')); } + public function testHandleWithLogChannel() + { + $request = new Request(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); + + $defaultLogger = new TestLogger(); + $channelLoger = new TestLogger(); + + $l = new ErrorListener('not used', $defaultLogger, false, [ + \RuntimeException::class => [ + 'log_level' => 'warning', + 'status_code' => 401, + 'log_channel' => 'channel', + ], + \Exception::class => [ + 'log_level' => 'error', + 'status_code' => 402, + ], + ], ['channel' => $channelLoger]); + + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertCount(0, $defaultLogger->getLogsForLevel('error')); + $this->assertCount(0, $defaultLogger->getLogsForLevel('warning')); + $this->assertCount(0, $channelLoger->getLogsForLevel('error')); + $this->assertCount(1, $channelLoger->getLogsForLevel('warning')); + } + + public function testHandleWithLoggerChannelNotUsed() + { + $request = new Request(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); + $defaultLogger = new TestLogger(); + $channelLoger = new TestLogger(); + $l = new ErrorListener('not used', $defaultLogger, false, [ + \RuntimeException::class => [ + 'log_level' => 'warning', + 'status_code' => 401, + ], + \ErrorException::class => [ + 'log_level' => 'error', + 'status_code' => 402, + 'log_channel' => 'channel', + ], + ], ['channel' => $channelLoger]); + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertSame(0, $defaultLogger->countErrors()); + $this->assertCount(0, $defaultLogger->getLogsForLevel('critical')); + $this->assertCount(1, $defaultLogger->getLogsForLevel('warning')); + $this->assertCount(0, $channelLoger->getLogsForLevel('warning')); + $this->assertCount(0, $channelLoger->getLogsForLevel('error')); + $this->assertCount(0, $channelLoger->getLogsForLevel('critical')); + } + public function testHandleClassImplementingInterfaceWithLogLevelAttribute() { $request = new Request(); From 5617b2e23c0196df04476290921ab4b70c106bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 28 Mar 2025 18:29:28 +0100 Subject: [PATCH 18/19] Remove always-true condition Introduced in symfony/symfony#45657 symfony/dependency-injection 6.4+ is required, so the class always exists --- .../RegisterControllerArgumentLocatorsPass.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index d473a2e6b0..a5fa06f17b 100644 --- a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -51,8 +51,6 @@ public function process(ContainerBuilder $container): void } } - $emptyAutowireAttributes = class_exists(Autowire::class) ? null : []; - foreach ($container->findTaggedServiceIds('controller.service_arguments', true) as $id => $tags) { $def = $container->getDefinition($id); $def->setPublic(true); @@ -129,7 +127,7 @@ public function process(ContainerBuilder $container): void /** @var \ReflectionParameter $p */ $type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?')); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; - $autowireAttributes = $autowire ? $emptyAutowireAttributes : []; + $autowireAttributes = null; $parsedName = $p->name; $k = null; @@ -155,7 +153,7 @@ public function process(ContainerBuilder $container): void $args[$p->name] = $bindingValue; continue; - } elseif (!$autowire || (!($autowireAttributes ??= $p->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF)) && (!$type || '\\' !== $target[0]))) { + } elseif (!$autowire || (!($autowireAttributes = $p->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF)) && (!$type || '\\' !== $target[0]))) { continue; } elseif (is_subclass_of($type, \UnitEnum::class)) { // do not attempt to register enum typed arguments if not already present in bindings From 610f13eca95059fb0af6eb164b4cf7ca6e980778 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 18 Apr 2025 14:51:48 +0200 Subject: [PATCH 19/19] Don't enable tracing unless the profiler is enabled --- Debug/TraceableEventDispatcher.php | 6 ++++++ Profiler/ProfilerStateChecker.php | 33 ++++++++++++++++++++++++++++++ composer.json | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 Profiler/ProfilerStateChecker.php diff --git a/Debug/TraceableEventDispatcher.php b/Debug/TraceableEventDispatcher.php index beca6bfb14..915862eddb 100644 --- a/Debug/TraceableEventDispatcher.php +++ b/Debug/TraceableEventDispatcher.php @@ -25,6 +25,9 @@ class TraceableEventDispatcher extends BaseTraceableEventDispatcher { protected function beforeDispatch(string $eventName, object $event): void { + if ($this->disabled?->__invoke()) { + return; + } switch ($eventName) { case KernelEvents::REQUEST: $event->getRequest()->attributes->set('_stopwatch_token', bin2hex(random_bytes(3))); @@ -57,6 +60,9 @@ protected function beforeDispatch(string $eventName, object $event): void protected function afterDispatch(string $eventName, object $event): void { + if ($this->disabled?->__invoke()) { + return; + } switch ($eventName) { case KernelEvents::CONTROLLER_ARGUMENTS: $this->stopwatch->start('controller', 'section'); diff --git a/Profiler/ProfilerStateChecker.php b/Profiler/ProfilerStateChecker.php new file mode 100644 index 0000000000..56cb4e3cc5 --- /dev/null +++ b/Profiler/ProfilerStateChecker.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Profiler; + +use Psr\Container\ContainerInterface; + +class ProfilerStateChecker +{ + public function __construct( + private ContainerInterface $container, + private bool $defaultEnabled, + ) { + } + + public function isProfilerEnabled(): bool + { + return $this->container->get('profiler')?->isEnabled() ?? $this->defaultEnabled; + } + + public function isProfilerDisabled(): bool + { + return !$this->isProfilerEnabled(); + } +} diff --git a/composer.json b/composer.json index e9cb077587..bb9f4ba617 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", "symfony/http-foundation": "^7.3", "symfony/polyfill-ctype": "^1.8", "psr/log": "^1|^2|^3"