diff --git a/Attribute/AsController.php b/Attribute/AsController.php index 0f2c91d45b..f0d10a8b33 100644 --- a/Attribute/AsController.php +++ b/Attribute/AsController.php @@ -21,7 +21,4 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_FUNCTION)] class AsController { - public function __construct() - { - } } diff --git a/Attribute/AsTargetedValueResolver.php b/Attribute/AsTargetedValueResolver.php index c58f0e6dd5..0635566174 100644 --- a/Attribute/AsTargetedValueResolver.php +++ b/Attribute/AsTargetedValueResolver.php @@ -17,8 +17,10 @@ #[\Attribute(\Attribute::TARGET_CLASS)] class AsTargetedValueResolver { - public function __construct( - public readonly ?string $name = null, - ) { + /** + * @param string|null $name The name with which the resolver can be targeted + */ + public function __construct(public readonly ?string $name = null) + { } } 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/Attribute/MapDateTime.php b/Attribute/MapDateTime.php index bfe48a8090..db6b4d613e 100644 --- a/Attribute/MapDateTime.php +++ b/Attribute/MapDateTime.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; /** * Controller parameter tag to configure DateTime arguments. @@ -19,6 +20,11 @@ #[\Attribute(\Attribute::TARGET_PARAMETER)] class MapDateTime extends ValueResolver { + /** + * @param string|null $format The DateTime format to use, @see https://php.net/datetime.format + * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases + * @param class-string|string $resolver The name of the resolver to use + */ public function __construct( public readonly ?string $format = null, bool $disabled = false, diff --git a/Attribute/MapQueryParameter.php b/Attribute/MapQueryParameter.php index bbc1fff273..486813a820 100644 --- a/Attribute/MapQueryParameter.php +++ b/Attribute/MapQueryParameter.php @@ -11,12 +11,15 @@ namespace Symfony\Component\HttpKernel\Attribute; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; /** * Can be used to pass a query parameter to a controller argument. * * @author Ruud Kamphuis + * @author Ionut Enache */ #[\Attribute(\Attribute::TARGET_PARAMETER)] final class MapQueryParameter extends ValueResolver @@ -24,7 +27,11 @@ 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 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()", 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( public ?string $name = null, @@ -32,6 +39,7 @@ public function __construct( public int $flags = 0, public array $options = [], string $resolver = QueryParameterValueResolver::class, + public int $validationFailedStatusCode = Response::HTTP_NOT_FOUND, ) { parent::__construct($resolver); } diff --git a/Attribute/MapQueryString.php b/Attribute/MapQueryString.php index 83722266ee..07418df85c 100644 --- a/Attribute/MapQueryString.php +++ b/Attribute/MapQueryString.php @@ -26,11 +26,18 @@ class MapQueryString extends ValueResolver { public ArgumentMetadata $metadata; + /** + * @param array $serializationContext The serialization context to use when deserializing the query string + * @param string|GroupSequence|array|null $validationGroups The validation groups to use when validating the query string mapping + * @param class-string $resolver The class name of the resolver to use + * @param int $validationFailedStatusCode The HTTP code to return if the validation fails + */ public function __construct( public readonly array $serializationContext = [], 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/Attribute/MapRequestPayload.php b/Attribute/MapRequestPayload.php index cbac606e83..cf086380c0 100644 --- a/Attribute/MapRequestPayload.php +++ b/Attribute/MapRequestPayload.php @@ -26,12 +26,21 @@ class MapRequestPayload extends ValueResolver { public ArgumentMetadata $metadata; + /** + * @param array|string|null $acceptFormat The payload formats to accept (i.e. "json", "xml") + * @param array $serializationContext The serialization context to use when deserializing the payload + * @param string|GroupSequence|array|null $validationGroups The validation groups to use when validating the query string mapping + * @param class-string $resolver The class name of the resolver to use + * @param int $validationFailedStatusCode The HTTP code to return if the validation fails + * @param class-string|string|null $type The element type for array deserialization + */ public function __construct( public readonly array|string|null $acceptFormat = null, public readonly array $serializationContext = [], public readonly string|GroupSequence|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY, + public readonly ?string $type = null, ) { parent::__construct($resolver); } diff --git a/Attribute/MapUploadedFile.php b/Attribute/MapUploadedFile.php new file mode 100644 index 0000000000..f90b511dc7 --- /dev/null +++ b/Attribute/MapUploadedFile.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\Attribute; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\Validator\Constraint; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class MapUploadedFile extends ValueResolver +{ + public ArgumentMetadata $metadata; + + public function __construct( + /** @var Constraint|array|null */ + public Constraint|array|null $constraints = null, + public ?string $name = null, + string $resolver = RequestPayloadValueResolver::class, + public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY, + ) { + parent::__construct($resolver); + } +} diff --git a/Attribute/ValueResolver.php b/Attribute/ValueResolver.php index 5875a27484..e295965fca 100644 --- a/Attribute/ValueResolver.php +++ b/Attribute/ValueResolver.php @@ -13,11 +13,15 @@ use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +/** + * Defines which value resolver should be used for a given parameter. + */ #[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] class ValueResolver { /** - * @param class-string|string $resolver + * @param class-string|string $resolver The class name of the resolver to use + * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases */ public function __construct( public string $resolver, diff --git a/Attribute/WithHttpStatus.php b/Attribute/WithHttpStatus.php index 718427aacc..18aa6246dd 100644 --- a/Attribute/WithHttpStatus.php +++ b/Attribute/WithHttpStatus.php @@ -12,13 +12,16 @@ namespace Symfony\Component\HttpKernel\Attribute; /** + * Defines the HTTP status code applied to an exception. + * * @author Dejan Angelov */ #[\Attribute(\Attribute::TARGET_CLASS)] class WithHttpStatus { /** - * @param array $headers + * @param int $statusCode The HTTP status code to use + * @param array $headers The HTTP headers to add to the response */ public function __construct( public readonly int $statusCode, diff --git a/Attribute/WithLogLevel.php b/Attribute/WithLogLevel.php index 762b077043..15e697dfd5 100644 --- a/Attribute/WithLogLevel.php +++ b/Attribute/WithLogLevel.php @@ -14,18 +14,20 @@ use Psr\Log\LogLevel; /** + * Defines the log level applied to an exception. + * * @author Dejan Angelov */ #[\Attribute(\Attribute::TARGET_CLASS)] final class WithLogLevel { /** - * @param LogLevel::* $level + * @param LogLevel::* $level The level to use to log the exception */ public function __construct(public readonly string $level) { if (!\defined('Psr\Log\LogLevel::'.strtoupper($this->level))) { - throw new \InvalidArgumentException(sprintf('Invalid log level "%s".', $this->level)); + throw new \InvalidArgumentException(\sprintf('Invalid log level "%s".', $this->level)); } } } diff --git a/Bundle/AbstractBundle.php b/Bundle/AbstractBundle.php index d24a2bc788..76f314ab97 100644 --- a/Bundle/AbstractBundle.php +++ b/Bundle/AbstractBundle.php @@ -50,7 +50,7 @@ public function getContainerExtension(): ?ExtensionInterface public function getPath(): string { - if (null === $this->path) { + if (!isset($this->path)) { $reflected = new \ReflectionObject($this); // assume the modern directory structure by default $this->path = \dirname($reflected->getFileName(), 2); diff --git a/Bundle/Bundle.php b/Bundle/Bundle.php index 09a19c480c..3b8006d6c3 100644 --- a/Bundle/Bundle.php +++ b/Bundle/Bundle.php @@ -24,15 +24,12 @@ */ abstract class Bundle implements BundleInterface { - protected $name; - protected $extension; - protected $path; - private string $namespace; + protected string $name; + protected ExtensionInterface|false|null $extension = null; + protected string $path; + protected ?ContainerInterface $container; - /** - * @var ContainerInterface|null - */ - protected $container; + private string $namespace; /** * @return void @@ -70,7 +67,7 @@ public function getContainerExtension(): ?ExtensionInterface if (null !== $extension) { if (!$extension instanceof ExtensionInterface) { - throw new \LogicException(sprintf('Extension "%s" must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface.', get_debug_type($extension))); + throw new \LogicException(\sprintf('Extension "%s" must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface.', get_debug_type($extension))); } // check naming convention @@ -78,7 +75,7 @@ public function getContainerExtension(): ?ExtensionInterface $expectedAlias = Container::underscore($basename); if ($expectedAlias != $extension->getAlias()) { - throw new \LogicException(sprintf('Users will expect the alias of the default extension of a bundle to be the underscored version of the bundle name ("%s"). You can override "Bundle::getContainerExtension()" if you want to use "%s" or another alias.', $expectedAlias, $extension->getAlias())); + throw new \LogicException(\sprintf('Users will expect the alias of the default extension of a bundle to be the underscored version of the bundle name ("%s"). You can override "Bundle::getContainerExtension()" if you want to use "%s" or another alias.', $expectedAlias, $extension->getAlias())); } $this->extension = $extension; diff --git a/Bundle/BundleExtension.php b/Bundle/BundleExtension.php index b80bc21f25..8392218a29 100644 --- a/Bundle/BundleExtension.php +++ b/Bundle/BundleExtension.php @@ -51,7 +51,7 @@ public function prepend(ContainerBuilder $container): void $this->subject->prependExtension($configurator, $container); }; - $this->executeConfiguratorCallback($container, $callback, $this->subject); + $this->executeConfiguratorCallback($container, $callback, $this->subject, true); } public function load(array $configs, ContainerBuilder $container): void diff --git a/Bundle/BundleInterface.php b/Bundle/BundleInterface.php index 400a9e0c92..36502e8962 100644 --- a/Bundle/BundleInterface.php +++ b/Bundle/BundleInterface.php @@ -67,8 +67,5 @@ public function getNamespace(): string; */ public function getPath(): string; - /** - * @return void - */ - public function setContainer(?ContainerInterface $container); + public function setContainer(?ContainerInterface $container): void; } diff --git a/CHANGELOG.md b/CHANGELOG.md index c1743b1d14..6bf1a60ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,51 @@ CHANGELOG ========= +7.3 +--- + + * 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 +--- + + * Remove `@internal` flag and add `@final` to `ServicesResetter` + * Add support for `SYMFONY_DISABLE_RESOURCE_TRACKING` env var + * Add support for configuring trusted proxies/headers/hosts via env vars + +7.1 +--- + + * Add method `isKernelTerminating()` to `ExceptionEvent` that allows to check if an exception was thrown while the kernel is being terminated + * Add `HttpException::fromStatusCode()` + * Add `$validationFailedStatusCode` argument to `#[MapQueryParameter]` that allows setting a custom HTTP status code when validation fails + * Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved + * Add `$type` argument to `#[MapRequestPayload]` that allows mapping a list of items + * The `Extension` class is marked as internal, extend the `Extension` class from the DependencyInjection component instead + * Deprecate `Extension::addAnnotatedClassesToCompile()` + * Deprecate `AddAnnotatedClassesToCachePass` + * Deprecate the `setAnnotatedClassCache()` and `getAnnotatedClassesToCompile()` methods of the `Kernel` class + * Add `#[MapUploadedFile]` attribute to fetch, validate, and inject uploaded files into controller arguments + +7.0 +--- + + * Add argument `$reflector` to `ArgumentResolverInterface::getArguments()` and `ArgumentMetadataFactoryInterface::createArgumentMetadata()` + * Remove `ArgumentValueResolverInterface`, use `ValueResolverInterface` instead + * Remove `StreamedResponseListener` + * Remove `AbstractSurrogate::$phpEscapeMap` + * Remove `HttpKernelInterface::MASTER_REQUEST` + * Remove `terminate_on_cache_hit` option from `HttpCache` + * Require explicit argument when calling `ConfigDataCollector::setKernel()`, `RouterListener::setCurrentRequest()` + * Remove `Kernel::stripComments()` + * Remove `FileLinkFormatter`, use `FileLinkFormatter` from the ErrorHandler component instead + * Remove `UriSigner`, use `UriSigner` from the HttpFoundation component instead + * Add argument `$buildDir` to `WarmableInterface` + * Add argument `$filter` to `Profiler::find()` and `FileProfilerStorage::find()` + 6.4 --- diff --git a/CacheClearer/CacheClearerInterface.php b/CacheClearer/CacheClearerInterface.php index 5ca4265624..f40ad9b562 100644 --- a/CacheClearer/CacheClearerInterface.php +++ b/CacheClearer/CacheClearerInterface.php @@ -20,8 +20,6 @@ interface CacheClearerInterface { /** * Clears any caches necessary. - * - * @return void */ - public function clear(string $cacheDir); + public function clear(string $cacheDir): void; } diff --git a/CacheClearer/ChainCacheClearer.php b/CacheClearer/ChainCacheClearer.php index 0c541f21b8..5a8205efe4 100644 --- a/CacheClearer/ChainCacheClearer.php +++ b/CacheClearer/ChainCacheClearer.php @@ -20,14 +20,12 @@ */ class ChainCacheClearer implements CacheClearerInterface { - private iterable $clearers; - /** * @param iterable $clearers */ - public function __construct(iterable $clearers = []) - { - $this->clearers = $clearers; + public function __construct( + private iterable $clearers = [], + ) { } public function clear(string $cacheDir): void diff --git a/CacheClearer/Psr6CacheClearer.php b/CacheClearer/Psr6CacheClearer.php index 3c99b74af3..e33b0fed1d 100644 --- a/CacheClearer/Psr6CacheClearer.php +++ b/CacheClearer/Psr6CacheClearer.php @@ -39,7 +39,7 @@ public function hasPool(string $name): bool public function getPool(string $name): CacheItemPoolInterface { if (!$this->hasPool($name)) { - throw new \InvalidArgumentException(sprintf('Cache pool not found: "%s".', $name)); + throw new \InvalidArgumentException(\sprintf('Cache pool not found: "%s".', $name)); } return $this->pools[$name]; @@ -51,16 +51,13 @@ public function getPool(string $name): CacheItemPoolInterface public function clearPool(string $name): bool { if (!isset($this->pools[$name])) { - throw new \InvalidArgumentException(sprintf('Cache pool not found: "%s".', $name)); + throw new \InvalidArgumentException(\sprintf('Cache pool not found: "%s".', $name)); } return $this->pools[$name]->clear(); } - /** - * @return void - */ - public function clear(string $cacheDir) + public function clear(string $cacheDir): void { foreach ($this->pools as $pool) { $pool->clear(); diff --git a/CacheWarmer/CacheWarmer.php b/CacheWarmer/CacheWarmer.php index f940ba4a72..0139937b35 100644 --- a/CacheWarmer/CacheWarmer.php +++ b/CacheWarmer/CacheWarmer.php @@ -18,10 +18,7 @@ */ abstract class CacheWarmer implements CacheWarmerInterface { - /** - * @return void - */ - protected function writeCacheFile(string $file, $content) + protected function writeCacheFile(string $file, $content): void { $tmpFile = @tempnam(\dirname($file), basename($file)); if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { @@ -30,6 +27,6 @@ protected function writeCacheFile(string $file, $content) return; } - throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file)); + throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $file)); } } diff --git a/CacheWarmer/CacheWarmerAggregate.php b/CacheWarmer/CacheWarmerAggregate.php index 47873fe183..421533df61 100644 --- a/CacheWarmer/CacheWarmerAggregate.php +++ b/CacheWarmer/CacheWarmerAggregate.php @@ -22,20 +22,17 @@ */ class CacheWarmerAggregate implements CacheWarmerInterface { - private iterable $warmers; - private bool $debug; - private ?string $deprecationLogsFilepath; private bool $optionalsEnabled = false; private bool $onlyOptionalsEnabled = false; /** * @param iterable $warmers */ - public function __construct(iterable $warmers = [], bool $debug = false, ?string $deprecationLogsFilepath = null) - { - $this->warmers = $warmers; - $this->debug = $debug; - $this->deprecationLogsFilepath = $deprecationLogsFilepath; + public function __construct( + private iterable $warmers = [], + private bool $debug = false, + private ?string $deprecationLogsFilepath = null, + ) { } public function enableOptionalWarmers(): void @@ -48,17 +45,8 @@ public function enableOnlyOptionalWarmers(): void $this->onlyOptionalsEnabled = $this->optionalsEnabled = true; } - /** - * @param string|null $buildDir - */ - public function warmUp(string $cacheDir, string|SymfonyStyle|null $buildDir = null, ?SymfonyStyle $io = null): array + public function warmUp(string $cacheDir, ?string $buildDir = null, ?SymfonyStyle $io = null): array { - if ($buildDir instanceof SymfonyStyle) { - trigger_deprecation('symfony/http-kernel', '6.4', 'Passing a "%s" as second argument of "%s()" is deprecated, pass it as third argument instead, after the build directory.', SymfonyStyle::class, __METHOD__); - $io = $buildDir; - $buildDir = null; - } - if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) { $collectedLogs = []; $previousHandler = set_error_handler(function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { @@ -105,15 +93,15 @@ public function warmUp(string $cacheDir, string|SymfonyStyle|null $buildDir = nu } $start = microtime(true); - foreach ((array) $warmer->warmUp($cacheDir, $buildDir) as $item) { + foreach ($warmer->warmUp($cacheDir, $buildDir) as $item) { if (is_dir($item) || (str_starts_with($item, \dirname($cacheDir)) && !is_file($item)) || ($buildDir && str_starts_with($item, \dirname($buildDir)) && !is_file($item))) { - throw new \LogicException(sprintf('"%s::warmUp()" should return a list of files or classes but "%s" is none of them.', $warmer::class, $item)); + throw new \LogicException(\sprintf('"%s::warmUp()" should return a list of files or classes but "%s" is none of them.', $warmer::class, $item)); } $preload[] = $item; } if ($io?->isDebug()) { - $io->info(sprintf('"%s" completed in %0.2fms.', $warmer::class, 1000 * (microtime(true) - $start))); + $io->info(\sprintf('"%s" completed in %0.2fms.', $warmer::class, 1000 * (microtime(true) - $start))); } } } finally { diff --git a/CacheWarmer/CacheWarmerInterface.php b/CacheWarmer/CacheWarmerInterface.php index 1f1740b7e2..d1c5101869 100644 --- a/CacheWarmer/CacheWarmerInterface.php +++ b/CacheWarmer/CacheWarmerInterface.php @@ -25,8 +25,6 @@ interface CacheWarmerInterface extends WarmableInterface * * A warmer should return true if the cache can be * generated incrementally and on-demand. - * - * @return bool */ - public function isOptional(); + public function isOptional(): bool; } diff --git a/CacheWarmer/WarmableInterface.php b/CacheWarmer/WarmableInterface.php index cd051b1add..7ffe3c0dfd 100644 --- a/CacheWarmer/WarmableInterface.php +++ b/CacheWarmer/WarmableInterface.php @@ -24,7 +24,7 @@ interface WarmableInterface * @param string $cacheDir Where warm-up artifacts should be stored * @param string|null $buildDir Where read-only artifacts should go; null when called after compile-time * - * @return string[] A list of classes or files to preload on PHP 7.4+ + * @return string[] A list of classes or files to preload */ - public function warmUp(string $cacheDir /* , string $buildDir = null */); + public function warmUp(string $cacheDir, ?string $buildDir = null): array; } diff --git a/Config/FileLocator.php b/Config/FileLocator.php index fb6bb10f1f..01fc757c43 100644 --- a/Config/FileLocator.php +++ b/Config/FileLocator.php @@ -21,12 +21,9 @@ */ class FileLocator extends BaseFileLocator { - private KernelInterface $kernel; - - public function __construct(KernelInterface $kernel) - { - $this->kernel = $kernel; - + public function __construct( + private KernelInterface $kernel, + ) { parent::__construct(); } diff --git a/Controller/ArgumentResolver.php b/Controller/ArgumentResolver.php index 23c2d7faa2..b09a92f02d 100644 --- a/Controller/ArgumentResolver.php +++ b/Controller/ArgumentResolver.php @@ -18,10 +18,10 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException; use Symfony\Contracts\Service\ServiceProviderInterface; @@ -34,16 +34,17 @@ final class ArgumentResolver implements ArgumentResolverInterface { private ArgumentMetadataFactoryInterface $argumentMetadataFactory; private iterable $argumentValueResolvers; - private ?ContainerInterface $namedResolvers; /** - * @param iterable $argumentValueResolvers + * @param iterable $argumentValueResolvers */ - public function __construct(?ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = [], ?ContainerInterface $namedResolvers = null) - { + public function __construct( + ?ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, + iterable $argumentValueResolvers = [], + private ?ContainerInterface $namedResolvers = null, + ) { $this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory(); $this->argumentValueResolvers = $argumentValueResolvers ?: self::getDefaultArgumentValueResolvers(); - $this->namedResolvers = $namedResolvers; } public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array @@ -60,7 +61,7 @@ public function getArguments(Request $request, callable $controller, ?\Reflectio if ($attribute->disabled) { $disabledResolvers[$attribute->resolver] = true; } elseif ($resolverName) { - throw new \LogicException(sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $metadata->getName(), $this->getPrettyName($controller))); + throw new \LogicException(\sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $metadata->getName(), $metadata->getControllerName())); } else { $resolverName = $attribute->resolver; } @@ -79,42 +80,53 @@ public function getArguments(Request $request, callable $controller, ?\Reflectio } } + $valueResolverExceptions = []; foreach ($argumentValueResolvers as $name => $resolver) { - if ((!$resolver instanceof ValueResolverInterface || $resolver instanceof TraceableValueResolver) && !$resolver->supports($request, $metadata)) { - continue; - } if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) { continue; } - $count = 0; - foreach ($resolver->resolve($request, $metadata) as $argument) { - ++$count; - $arguments[] = $argument; + try { + $count = 0; + foreach ($resolver->resolve($request, $metadata) as $argument) { + ++$count; + $arguments[] = $argument; + } + } catch (NearMissValueResolverException $e) { + $valueResolverExceptions[] = $e; } if (1 < $count && !$metadata->isVariadic()) { - throw new \InvalidArgumentException(sprintf('"%s::resolve()" must yield at most one value for non-variadic arguments.', get_debug_type($resolver))); + throw new \InvalidArgumentException(\sprintf('"%s::resolve()" must yield at most one value for non-variadic arguments.', get_debug_type($resolver))); } if ($count) { // continue to the next controller argument continue 2; } + } + + $reasons = array_map(static fn (NearMissValueResolverException $e) => $e->getMessage(), $valueResolverExceptions); + if (!$reasons) { + $reasons[] = 'Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.'; + } - if (!$resolver instanceof ValueResolverInterface) { - throw new \InvalidArgumentException(sprintf('"%s::resolve()" must yield at least one value.', get_debug_type($resolver))); + $reasonCounter = 1; + if (\count($reasons) > 1) { + foreach ($reasons as $i => $reason) { + $reasons[$i] = $reasonCounter.') '.$reason; + ++$reasonCounter; } } - throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.', $this->getPrettyName($controller), $metadata->getName())); + throw new \RuntimeException(\sprintf('Controller "%s" requires the "$%s" argument that could not be resolved. '.($reasonCounter > 1 ? 'Possible reasons: ' : '').'%s', $metadata->getControllerName(), $metadata->getName(), implode(' ', $reasons))); } return $arguments; } /** - * @return iterable + * @return iterable */ public static function getDefaultArgumentValueResolvers(): iterable { @@ -126,21 +138,4 @@ public static function getDefaultArgumentValueResolvers(): iterable new VariadicValueResolver(), ]; } - - private function getPrettyName($controller): string - { - if (\is_array($controller)) { - if (\is_object($controller[0])) { - $controller[0] = get_debug_type($controller[0]); - } - - return $controller[0].'::'.$controller[1]; - } - - if (\is_object($controller)) { - return get_debug_type($controller); - } - - return $controller; - } } diff --git a/Controller/ArgumentResolver/BackedEnumValueResolver.php b/Controller/ArgumentResolver/BackedEnumValueResolver.php index 620e2de080..9193cee060 100644 --- a/Controller/ArgumentResolver/BackedEnumValueResolver.php +++ b/Controller/ArgumentResolver/BackedEnumValueResolver.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -22,33 +21,9 @@ * leading to a 404 Not Found if the attribute value isn't a valid backing value for the enum type. * * @author Maxime Steinhausser - * - * @final since Symfony 6.2 */ -class BackedEnumValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class BackedEnumValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { - return false; - } - - if ($argument->isVariadic()) { - // only target route path parameters, which cannot be variadic. - return false; - } - - // do not support if no value can be resolved at all - // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used - // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. - return $request->attributes->has($argument->getName()); - } - public function resolve(Request $request, ArgumentMetadata $argument): iterable { if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { @@ -78,7 +53,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable } if (!\is_int($value) && !\is_string($value)) { - throw new \LogicException(sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', $argument->getType(), $argument->getName(), get_debug_type($value))); + throw new \LogicException(\sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', $argument->getType(), $argument->getName(), get_debug_type($value))); } /** @var class-string<\BackedEnum> $enumType */ @@ -87,7 +62,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable try { return [$enumType::from($value)]; } catch (\ValueError|\TypeError $e) { - throw new NotFoundHttpException(sprintf('Could not resolve the "%s $%s" controller argument: ', $argument->getType(), $argument->getName()).$e->getMessage(), $e); + throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: ', $argument->getType(), $argument->getName()).$e->getMessage(), $e); } } } diff --git a/Controller/ArgumentResolver/DateTimeValueResolver.php b/Controller/ArgumentResolver/DateTimeValueResolver.php index 0cfd42badc..10ea8826f9 100644 --- a/Controller/ArgumentResolver/DateTimeValueResolver.php +++ b/Controller/ArgumentResolver/DateTimeValueResolver.php @@ -14,7 +14,6 @@ use Psr\Clock\ClockInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -25,23 +24,13 @@ * @author Benjamin Eberlei * @author Tim Goudriaan */ -final class DateTimeValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class DateTimeValueResolver implements ValueResolverInterface { public function __construct( private readonly ?ClockInterface $clock = null, ) { } - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - return is_a($argument->getType(), \DateTimeInterface::class, true) && $request->attributes->has($argument->getName()); - } - public function resolve(Request $request, ArgumentMetadata $argument): array { if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$request->attributes->has($argument->getName())) { @@ -90,7 +79,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array } if (!$date) { - throw new NotFoundHttpException(sprintf('Invalid date given for parameter "%s".', $argument->getName())); + throw new NotFoundHttpException(\sprintf('Invalid date given for parameter "%s".', $argument->getName())); } return [$date]; diff --git a/Controller/ArgumentResolver/DefaultValueResolver.php b/Controller/ArgumentResolver/DefaultValueResolver.php index eb9769c09a..bf114f3f31 100644 --- a/Controller/ArgumentResolver/DefaultValueResolver.php +++ b/Controller/ArgumentResolver/DefaultValueResolver.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -21,18 +20,8 @@ * * @author Iltar van der Berg */ -final class DefaultValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class DefaultValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - return $argument->hasDefaultValue() || (null !== $argument->getType() && $argument->isNullable() && !$argument->isVariadic()); - } - public function resolve(Request $request, ArgumentMetadata $argument): array { if ($argument->hasDefaultValue()) { diff --git a/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php b/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php index 2640361288..c5c862e667 100644 --- a/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php +++ b/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php @@ -14,7 +14,6 @@ use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -23,39 +22,11 @@ * * @author Simeon Kolev */ -final class NotTaggedControllerValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class NotTaggedControllerValueResolver implements ValueResolverInterface { - private ContainerInterface $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - $controller = $request->attributes->get('_controller'); - - if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { - $controller = $controller[0].'::'.$controller[1]; - } elseif (!\is_string($controller) || '' === $controller) { - return false; - } - - if ('\\' === $controller[0]) { - $controller = ltrim($controller, '\\'); - } - - if (!$this->container->has($controller) && false !== $i = strrpos($controller, ':')) { - $controller = substr($controller, 0, $i).strtolower(substr($controller, $i)); - } - - return false === $this->container->has($controller); + public function __construct( + private ContainerInterface $container, + ) { } public function resolve(Request $request, ArgumentMetadata $argument): array @@ -82,8 +53,8 @@ public function resolve(Request $request, ArgumentMetadata $argument): array return []; } - $what = sprintf('argument $%s of "%s()"', $argument->getName(), $controller); - $message = sprintf('Could not resolve %s, maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?', $what); + $what = \sprintf('argument $%s of "%s()"', $argument->getName(), $controller); + $message = \sprintf('Could not resolve %s, maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?', $what); throw new RuntimeException($message); } diff --git a/Controller/ArgumentResolver/QueryParameterValueResolver.php b/Controller/ArgumentResolver/QueryParameterValueResolver.php index b186a39c59..5fe3d75313 100644 --- a/Controller/ArgumentResolver/QueryParameterValueResolver.php +++ b/Controller/ArgumentResolver/QueryParameterValueResolver.php @@ -15,7 +15,8 @@ use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\Uid\AbstractUid; /** * Resolve arguments of type: array, string, int, float, bool, \BackedEnum from query parameters. @@ -23,6 +24,7 @@ * @author Ruud Kamphuis * @author Nicolas Grekas * @author Mateusz Anders + * @author Ionut Enache */ final class QueryParameterValueResolver implements ValueResolverInterface { @@ -33,12 +35,14 @@ public function resolve(Request $request, ArgumentMetadata $argument): array } $name = $attribute->name ?? $argument->getName(); + $validationFailedCode = $attribute->validationFailedStatusCode; + if (!$request->query->has($name)) { if ($argument->isNullable() || $argument->hasDefaultValue()) { return []; } - throw new NotFoundHttpException(sprintf('Missing query parameter "%s".', $name)); + throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Missing query parameter "%s".', $name)); } $value = $request->query->all()[$name]; @@ -52,7 +56,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $filtered = array_values(array_filter((array) $value, \is_array(...))); if ($filtered !== $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { - throw new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name)); + throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } return $filtered; @@ -70,18 +74,25 @@ 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, - '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, + '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')), + }, }; $value = filter_var($value, $attribute->filter ?? $filter, $options); @@ -102,8 +113,12 @@ 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 new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name)); + throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } if (!\is_array($value)) { @@ -117,7 +132,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array } if ($filtered !== $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { - throw new NotFoundHttpException(sprintf('Invalid query parameter "%s".', $name)); + throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } return $argument->isVariadic() ? $filtered : [$filtered]; diff --git a/Controller/ArgumentResolver/RequestAttributeValueResolver.php b/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 370e414451..2a8d48ee30 100644 --- a/Controller/ArgumentResolver/RequestAttributeValueResolver.php +++ b/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -21,18 +20,8 @@ * * @author Iltar van der Berg */ -final class RequestAttributeValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class RequestAttributeValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - return !$argument->isVariadic() && $request->attributes->has($argument->getName()); - } - public function resolve(Request $request, ArgumentMetadata $argument): array { return !$argument->isVariadic() && $request->attributes->has($argument->getName()) ? [$request->attributes->get($argument->getName())] : []; diff --git a/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/Controller/ArgumentResolver/RequestPayloadValueResolver.php index f0f735da42..0ca965db4b 100644 --- a/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -12,20 +12,26 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; +use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; use Symfony\Component\Serializer\Exception\UnsupportedFormatException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\ValidationFailedException; @@ -57,6 +63,7 @@ public function __construct( private readonly SerializerInterface&DenormalizerInterface $serializer, private readonly ?ValidatorInterface $validator = null, private readonly ?TranslatorInterface $translator = null, + private string $translationDomain = 'validators', ) { } @@ -64,14 +71,25 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable { $attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? $argument->getAttributesOfType(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] + ?? $argument->getAttributesOfType(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null; if (!$attribute) { return []; } - if ($argument->isVariadic()) { - throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())); + if (!$attribute instanceof MapUploadedFile && $argument->isVariadic()) { + throw new \LogicException(\sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())); + } + + if ($attribute instanceof MapRequestPayload) { + if ('array' === $argument->getType()) { + if (!$attribute->type) { + throw new NearMissValueResolverException(\sprintf('Please set the $type argument of the #[%s] attribute to the type of the objects in the expected array.', MapRequestPayload::class)); + } + } elseif ($attribute->type) { + throw new NearMissValueResolverException(\sprintf('Please set its type to "array" when using argument $type of #[%s].', MapRequestPayload::class)); + } } $attribute->metadata = $argument; @@ -85,24 +103,27 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo foreach ($arguments as $i => $argument) { if ($argument instanceof MapQueryString) { - $payloadMapper = 'mapQueryString'; + $payloadMapper = $this->mapQueryString(...); $validationFailedCode = $argument->validationFailedStatusCode; } elseif ($argument instanceof MapRequestPayload) { - $payloadMapper = 'mapRequestPayload'; + $payloadMapper = $this->mapRequestPayload(...); + $validationFailedCode = $argument->validationFailedStatusCode; + } elseif ($argument instanceof MapUploadedFile) { + $payloadMapper = $this->mapUploadedFile(...); $validationFailedCode = $argument->validationFailedStatusCode; } else { continue; } $request = $event->getRequest(); - if (!$type = $argument->metadata->getType()) { - throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName())); + if (!$argument->metadata->getType()) { + throw new \LogicException(\sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName())); } if ($this->validator) { $violations = new ConstraintViolationList(); try { - $payload = $this->$payloadMapper($request, $type, $argument); + $payload = $payloadMapper($request, $argument->metadata, $argument); } catch (PartialDenormalizationException $e) { $trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p); foreach ($e->getErrors() as $error) { @@ -115,24 +136,28 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo if ($error->canUseMessageForUser()) { $parameters['hint'] = $error->getMessage(); } - $message = $trans($template, $parameters, 'validators'); + $message = $trans($template, $parameters, $this->translationDomain); $violations->add(new ConstraintViolation($message, $template, $parameters, null, $error->getPath(), null)); } $payload = $e->getData(); } if (null !== $payload && !\count($violations)) { - $violations->addAll($this->validator->validate($payload, null, $argument->validationGroups ?? null)); + $constraints = $argument->constraints ?? null; + if (\is_array($payload) && !empty($constraints) && !$constraints instanceof Assert\All) { + $constraints = new Assert\All($constraints); + } + $violations->addAll($this->validator->validate($payload, $constraints, $argument->validationGroups ?? null)); } if (\count($violations)) { - throw new HttpException($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations)); + throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations)); } } else { try { - $payload = $this->$payloadMapper($request, $type, $argument); + $payload = $payloadMapper($request, $argument->metadata, $argument); } catch (PartialDenormalizationException $e) { - throw new HttpException($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e); + throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e); } } @@ -140,7 +165,7 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo $payload = match (true) { $argument->metadata->hasDefaultValue() => $argument->metadata->getDefaultValue(), $argument->metadata->isNullable() => null, - default => throw new HttpException($validationFailedCode) + default => throw HttpException::fromStatusCode($validationFailedCode), }; } @@ -157,43 +182,60 @@ public static function getSubscribedEvents(): array ]; } - private function mapQueryString(Request $request, string $type, MapQueryString $attribute): ?object + private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object { - if (!$data = $request->query->all()) { + if (!($data = $request->query->all($attribute->key)) && ($argument->isNullable() || $argument->hasDefaultValue())) { return null; } - return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE); + return $this->serializer->denormalize($data, $argument->getType(), 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]); } - private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object + private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null { if (null === $format = $request->getContentTypeFormat()) { - throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, 'Unsupported format.'); + throw new UnsupportedMediaTypeHttpException('Unsupported format.'); } if ($attribute->acceptFormat && !\in_array($format, (array) $attribute->acceptFormat, true)) { - throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); + throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); + } + + if ('array' === $argument->getType() && null !== $attribute->type) { + $type = $attribute->type.'[]'; + } else { + $type = $argument->getType(); } if ($data = $request->request->all()) { - return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE); + return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : [])); } - if ('' === $data = $request->getContent()) { + if ('' === ($data = $request->getContent()) && ($argument->isNullable() || $argument->hasDefaultValue())) { return null; } if ('form' === $format) { - throw new HttpException(Response::HTTP_BAD_REQUEST, 'Request payload contains invalid "form" data.'); + throw new BadRequestHttpException('Request payload contains invalid "form" data.'); } try { return $this->serializer->deserialize($data, $type, $format, self::CONTEXT_DESERIALIZE + $attribute->serializationContext); } catch (UnsupportedFormatException $e) { - throw new HttpException(Response::HTTP_UNSUPPORTED_MEDIA_TYPE, sprintf('Unsupported format: "%s".', $format), $e); + throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format: "%s".', $format), $e); } catch (NotEncodableValueException $e) { - throw new HttpException(Response::HTTP_BAD_REQUEST, sprintf('Request payload contains invalid "%s" data.', $format), $e); + throw new BadRequestHttpException(\sprintf('Request payload contains invalid "%s" data.', $format), $e); + } catch (UnexpectedPropertyException $e) { + throw new BadRequestHttpException(\sprintf('Request payload contains invalid "%s" property.', $e->property), $e); } } + + private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null + { + if (!($files = $request->files->get($attribute->name ?? $argument->getName(), [])) && ($argument->isNullable() || $argument->hasDefaultValue())) { + return null; + } + + return $files; + } } diff --git a/Controller/ArgumentResolver/RequestValueResolver.php b/Controller/ArgumentResolver/RequestValueResolver.php index 6347f70196..28e41181e4 100644 --- a/Controller/ArgumentResolver/RequestValueResolver.php +++ b/Controller/ArgumentResolver/RequestValueResolver.php @@ -12,29 +12,27 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; /** * Yields the same instance as the request object passed along. * * @author Iltar van der Berg */ -final class RequestValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class RequestValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool + public function resolve(Request $request, ArgumentMetadata $argument): array { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); + if (Request::class === $argument->getType() || is_subclass_of($argument->getType(), Request::class)) { + return [$request]; + } - return Request::class === $argument->getType() || is_subclass_of($argument->getType(), Request::class); - } + if (str_ends_with($argument->getType() ?? '', '\\Request')) { + throw new NearMissValueResolverException(\sprintf('Looks like you required a Request object with the wrong class name "%s". Did you mean to use "%s" instead?', $argument->getType(), Request::class)); + } - public function resolve(Request $request, ArgumentMetadata $argument): array - { - return Request::class === $argument->getType() || is_subclass_of($argument->getType(), Request::class) ? [$request] : []; + return []; } } diff --git a/Controller/ArgumentResolver/ServiceValueResolver.php b/Controller/ArgumentResolver/ServiceValueResolver.php index 96e0337d6a..62074ef00a 100644 --- a/Controller/ArgumentResolver/ServiceValueResolver.php +++ b/Controller/ArgumentResolver/ServiceValueResolver.php @@ -14,48 +14,20 @@ use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; /** * Yields a service keyed by _controller and argument name. * * @author Nicolas Grekas */ -final class ServiceValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class ServiceValueResolver implements ValueResolverInterface { - private ContainerInterface $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - $controller = $request->attributes->get('_controller'); - - if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { - $controller = $controller[0].'::'.$controller[1]; - } elseif (!\is_string($controller) || '' === $controller) { - return false; - } - - if ('\\' === $controller[0]) { - $controller = ltrim($controller, '\\'); - } - - if (!$this->container->has($controller) && false !== $i = strrpos($controller, ':')) { - $controller = substr($controller, 0, $i).strtolower(substr($controller, $i)); - } - - return $this->container->has($controller) && $this->container->get($controller)->has($argument->getName()); + public function __construct( + private ContainerInterface $container, + ) { } public function resolve(Request $request, ArgumentMetadata $argument): array @@ -83,17 +55,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): array try { return [$this->container->get($controller)->get($argument->getName())]; } catch (RuntimeException $e) { - $what = sprintf('argument $%s of "%s()"', $argument->getName(), $controller); - $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $e->getMessage()); + $what = 'argument $'.$argument->getName(); + $message = str_replace(\sprintf('service "%s"', $argument->getName()), $what, $e->getMessage()); + $what .= \sprintf(' of "%s()"', $controller); + $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message); if ($e->getMessage() === $message) { - $message = sprintf('Cannot resolve %s: %s', $what, $message); + $message = \sprintf('Cannot resolve %s: %s', $what, $message); } - $r = new \ReflectionProperty($e, 'message'); - $r->setValue($e, $message); - - throw $e; + throw new NearMissValueResolverException($message, $e->getCode(), $e); } } } diff --git a/Controller/ArgumentResolver/SessionValueResolver.php b/Controller/ArgumentResolver/SessionValueResolver.php index c8e7575d53..30b7f1d749 100644 --- a/Controller/ArgumentResolver/SessionValueResolver.php +++ b/Controller/ArgumentResolver/SessionValueResolver.php @@ -13,7 +13,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -22,27 +21,8 @@ * * @author Iltar van der Berg */ -final class SessionValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class SessionValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - if (!$request->hasSession()) { - return false; - } - - $type = $argument->getType(); - if (SessionInterface::class !== $type && !is_subclass_of($type, SessionInterface::class)) { - return false; - } - - return $request->getSession() instanceof $type; - } - public function resolve(Request $request, ArgumentMetadata $argument): array { if (!$request->hasSession()) { diff --git a/Controller/ArgumentResolver/TraceableValueResolver.php b/Controller/ArgumentResolver/TraceableValueResolver.php index 0cb4703b29..41fd1d9ae9 100644 --- a/Controller/ArgumentResolver/TraceableValueResolver.php +++ b/Controller/ArgumentResolver/TraceableValueResolver.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Stopwatch\Stopwatch; @@ -22,34 +21,12 @@ * * @author Iltar van der Berg */ -final class TraceableValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class TraceableValueResolver implements ValueResolverInterface { - private ArgumentValueResolverInterface|ValueResolverInterface $inner; - private Stopwatch $stopwatch; - - public function __construct(ArgumentValueResolverInterface|ValueResolverInterface $inner, Stopwatch $stopwatch) - { - $this->inner = $inner; - $this->stopwatch = $stopwatch; - } - - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - if ($this->inner instanceof ValueResolverInterface) { - return true; - } - - $method = $this->inner::class.'::'.__FUNCTION__; - $this->stopwatch->start($method, 'controller.argument_value_resolver'); - - $return = $this->inner->supports($request, $argument); - - $this->stopwatch->stop($method); - - return $return; + public function __construct( + private ValueResolverInterface $inner, + private Stopwatch $stopwatch, + ) { } public function resolve(Request $request, ArgumentMetadata $argument): iterable diff --git a/Controller/ArgumentResolver/UidValueResolver.php b/Controller/ArgumentResolver/UidValueResolver.php index 7a12e21ead..4a232eb8ae 100644 --- a/Controller/ArgumentResolver/UidValueResolver.php +++ b/Controller/ArgumentResolver/UidValueResolver.php @@ -12,27 +12,13 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Uid\AbstractUid; -final class UidValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class UidValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - return !$argument->isVariadic() - && \is_string($request->attributes->get($argument->getName())) - && null !== $argument->getType() - && is_subclass_of($argument->getType(), AbstractUid::class, true); - } - public function resolve(Request $request, ArgumentMetadata $argument): array { if ($argument->isVariadic() @@ -46,7 +32,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array try { return [$uidClass::fromString($value)]; } catch (\InvalidArgumentException $e) { - throw new NotFoundHttpException(sprintf('The uid for the "%s" parameter is invalid.', $argument->getName()), $e); + throw new NotFoundHttpException(\sprintf('The uid for the "%s" parameter is invalid.', $argument->getName()), $e); } } } diff --git a/Controller/ArgumentResolver/VariadicValueResolver.php b/Controller/ArgumentResolver/VariadicValueResolver.php index 4f6cba729e..d046129f4f 100644 --- a/Controller/ArgumentResolver/VariadicValueResolver.php +++ b/Controller/ArgumentResolver/VariadicValueResolver.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -21,18 +20,8 @@ * * @author Iltar van der Berg */ -final class VariadicValueResolver implements ArgumentValueResolverInterface, ValueResolverInterface +final class VariadicValueResolver implements ValueResolverInterface { - /** - * @deprecated since Symfony 6.2, use resolve() instead - */ - public function supports(Request $request, ArgumentMetadata $argument): bool - { - @trigger_deprecation('symfony/http-kernel', '6.2', 'The "%s()" method is deprecated, use "resolve()" instead.', __METHOD__); - - return $argument->isVariadic() && $request->attributes->has($argument->getName()); - } - public function resolve(Request $request, ArgumentMetadata $argument): array { if (!$argument->isVariadic() || !$request->attributes->has($argument->getName())) { @@ -42,7 +31,7 @@ public function resolve(Request $request, ArgumentMetadata $argument): array $values = $request->attributes->get($argument->getName()); if (!\is_array($values)) { - throw new \InvalidArgumentException(sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); + throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); } return $values; diff --git a/Controller/ArgumentResolverInterface.php b/Controller/ArgumentResolverInterface.php index 33d3ce2985..2090a59928 100644 --- a/Controller/ArgumentResolverInterface.php +++ b/Controller/ArgumentResolverInterface.php @@ -24,9 +24,7 @@ interface ArgumentResolverInterface /** * Returns the arguments to pass to the controller. * - * @param \ReflectionFunctionAbstract|null $reflector - * * @throws \RuntimeException When no value could be provided for a required argument */ - public function getArguments(Request $request, callable $controller/* , \ReflectionFunctionAbstract $reflector = null */): array; + public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array; } diff --git a/Controller/ArgumentValueResolverInterface.php b/Controller/ArgumentValueResolverInterface.php deleted file mode 100644 index 9c3b1a0162..0000000000 --- a/Controller/ArgumentValueResolverInterface.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\Controller; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; - -/** - * Responsible for resolving the value of an argument based on its metadata. - * - * @author Iltar van der Berg - * - * @deprecated since Symfony 6.2, implement ValueResolverInterface instead - */ -interface ArgumentValueResolverInterface -{ - /** - * Whether this resolver can resolve the value for the given ArgumentMetadata. - */ - public function supports(Request $request, ArgumentMetadata $argument): bool; - - /** - * Returns the possible value(s). - */ - public function resolve(Request $request, ArgumentMetadata $argument): iterable; -} diff --git a/Controller/ContainerControllerResolver.php b/Controller/ContainerControllerResolver.php index 12232d58b5..3699fc0254 100644 --- a/Controller/ContainerControllerResolver.php +++ b/Controller/ContainerControllerResolver.php @@ -23,12 +23,10 @@ */ class ContainerControllerResolver extends ControllerResolver { - protected $container; - - public function __construct(ContainerInterface $container, ?LoggerInterface $logger = null) - { - $this->container = $container; - + public function __construct( + protected ContainerInterface $container, + ?LoggerInterface $logger = null, + ) { parent::__construct($logger); } @@ -48,16 +46,16 @@ protected function instantiateController(string $class): object $this->throwExceptionIfControllerWasRemoved($class, $e); if ($e instanceof \ArgumentCountError) { - throw new \InvalidArgumentException(sprintf('Controller "%s" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', $class), 0, $e); + throw new \InvalidArgumentException(\sprintf('Controller "%s" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', $class), 0, $e); } - throw new \InvalidArgumentException(sprintf('Controller "%s" does neither exist as service nor as class.', $class), 0, $e); + throw new \InvalidArgumentException(\sprintf('Controller "%s" does neither exist as service nor as class.', $class), 0, $e); } private function throwExceptionIfControllerWasRemoved(string $controller, \Throwable $previous): void { if ($this->container instanceof Container && isset($this->container->getRemovedIds()[$controller])) { - throw new \InvalidArgumentException(sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $controller), 0, $previous); + throw new \InvalidArgumentException(\sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $controller), 0, $previous); } } } diff --git a/Controller/ControllerReference.php b/Controller/ControllerReference.php index b4fdadd21e..0ecdc29b1d 100644 --- a/Controller/ControllerReference.php +++ b/Controller/ControllerReference.php @@ -26,18 +26,19 @@ */ class ControllerReference { - public $controller; - public $attributes = []; - public $query = []; + public array $attributes = []; + public array $query = []; /** * @param string $controller The controller name * @param array $attributes An array of parameters to add to the Request attributes * @param array $query An array of parameters to add to the Request query string */ - public function __construct(string $controller, array $attributes = [], array $query = []) - { - $this->controller = $controller; + public function __construct( + public string $controller, + array $attributes = [], + array $query = [], + ) { $this->attributes = $attributes; $this->query = $query; } diff --git a/Controller/ControllerResolver.php b/Controller/ControllerResolver.php index 8424b02cc1..12bcd4c2dd 100644 --- a/Controller/ControllerResolver.php +++ b/Controller/ControllerResolver.php @@ -25,13 +25,12 @@ */ class ControllerResolver implements ControllerResolverInterface { - private ?LoggerInterface $logger; private array $allowedControllerTypes = []; private array $allowedControllerAttributes = [AsController::class => AsController::class]; - public function __construct(?LoggerInterface $logger = null) - { - $this->logger = $logger; + public function __construct( + private ?LoggerInterface $logger = null, + ) { } /** @@ -74,7 +73,7 @@ public function getController(Request $request): callable|false } if (!\is_callable($controller)) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); + throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); } return $this->checkController($request, $controller); @@ -82,7 +81,7 @@ public function getController(Request $request): callable|false if (\is_object($controller)) { if (!\is_callable($controller)) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); + throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); } return $this->checkController($request, $controller); @@ -95,11 +94,11 @@ public function getController(Request $request): callable|false try { $callable = $this->createController($controller); } catch (\InvalidArgumentException $e) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$e->getMessage(), 0, $e); + throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$e->getMessage(), 0, $e); } if (!\is_callable($callable)) { - throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($callable)); + throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($callable)); } return $this->checkController($request, $callable); @@ -159,19 +158,19 @@ private function getControllerError(mixed $callable): string if (str_contains($callable, '::')) { $callable = explode('::', $callable, 2); } else { - return sprintf('Function "%s" does not exist.', $callable); + return \sprintf('Function "%s" does not exist.', $callable); } } if (\is_object($callable)) { $availableMethods = $this->getClassMethodsWithoutMagicMethods($callable); - $alternativeMsg = $availableMethods ? sprintf(' or use one of the available methods: "%s"', implode('", "', $availableMethods)) : ''; + $alternativeMsg = $availableMethods ? \sprintf(' or use one of the available methods: "%s"', implode('", "', $availableMethods)) : ''; - return sprintf('Controller class "%s" cannot be called without a method name. You need to implement "__invoke"%s.', get_debug_type($callable), $alternativeMsg); + return \sprintf('Controller class "%s" cannot be called without a method name. You need to implement "__invoke"%s.', get_debug_type($callable), $alternativeMsg); } if (!\is_array($callable)) { - return sprintf('Invalid type for controller given, expected string, array or object, got "%s".', get_debug_type($callable)); + return \sprintf('Invalid type for controller given, expected string, array or object, got "%s".', get_debug_type($callable)); } if (!isset($callable[0]) || !isset($callable[1]) || 2 !== \count($callable)) { @@ -181,13 +180,13 @@ private function getControllerError(mixed $callable): string [$controller, $method] = $callable; if (\is_string($controller) && !class_exists($controller)) { - return sprintf('Class "%s" does not exist.', $controller); + return \sprintf('Class "%s" does not exist.', $controller); } $className = \is_object($controller) ? get_debug_type($controller) : $controller; if (method_exists($controller, $method)) { - return sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className); + return \sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className); } $collection = $this->getClassMethodsWithoutMagicMethods($controller); @@ -204,12 +203,12 @@ private function getControllerError(mixed $callable): string asort($alternatives); - $message = sprintf('Expected method "%s" on class "%s"', $method, $className); + $message = \sprintf('Expected method "%s" on class "%s"', $method, $className); if (\count($alternatives) > 0) { - $message .= sprintf(', did you mean "%s"?', implode('", "', $alternatives)); + $message .= \sprintf(', did you mean "%s"?', implode('", "', $alternatives)); } else { - $message .= sprintf('. Available methods: "%s".', implode('", "', $collection)); + $message .= \sprintf('. Available methods: "%s".', implode('", "', $collection)); } return $message; @@ -240,9 +239,9 @@ private function checkController(Request $request, callable $controller): callab $r = new \ReflectionFunction($controller); $name = $r->name; - if (str_contains($name, '{closure')) { + if ($r->isAnonymous()) { $name = $class = \Closure::class; - } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + } elseif ($class = $r->getClosureCalledClass()) { $class = $class->name; $name = $class.'::'.$name; } @@ -268,12 +267,6 @@ private function checkController(Request $request, callable $controller): callab $name = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $name); } - if (-1 === $request->attributes->get('_check_controller_is_allowed')) { - trigger_deprecation('symfony/http-kernel', '6.4', 'Callable "%s()" is not allowed as a controller. Did you miss tagging it with "#[AsController]" or registering its type with "%s::allowControllers()"?', $name, self::class); - - return $controller; - } - - throw new BadRequestException(sprintf('Callable "%s()" is not allowed as a controller. Did you miss tagging it with "#[AsController]" or registering its type with "%s::allowControllers()"?', $name, self::class)); + throw new BadRequestException(\sprintf('Callable "%s()" is not allowed as a controller. Did you miss tagging it with "#[AsController]" or registering its type with "%s::allowControllers()"?', $name, self::class)); } } diff --git a/Controller/ErrorController.php b/Controller/ErrorController.php index 9dd2116866..616920e5d1 100644 --- a/Controller/ErrorController.php +++ b/Controller/ErrorController.php @@ -25,15 +25,11 @@ */ class ErrorController { - private HttpKernelInterface $kernel; - private string|object|array|null $controller; - private ErrorRendererInterface $errorRenderer; - - public function __construct(HttpKernelInterface $kernel, string|object|array|null $controller, ErrorRendererInterface $errorRenderer) - { - $this->kernel = $kernel; - $this->controller = $controller; - $this->errorRenderer = $errorRenderer; + public function __construct( + private HttpKernelInterface $kernel, + private string|object|array|null $controller, + private ErrorRendererInterface $errorRenderer, + ) { } public function __invoke(\Throwable $exception): Response diff --git a/Controller/TraceableArgumentResolver.php b/Controller/TraceableArgumentResolver.php index 27cc8fb1ae..c6dac59373 100644 --- a/Controller/TraceableArgumentResolver.php +++ b/Controller/TraceableArgumentResolver.php @@ -19,21 +19,14 @@ */ class TraceableArgumentResolver implements ArgumentResolverInterface { - private ArgumentResolverInterface $resolver; - private Stopwatch $stopwatch; - - public function __construct(ArgumentResolverInterface $resolver, Stopwatch $stopwatch) - { - $this->resolver = $resolver; - $this->stopwatch = $stopwatch; + public function __construct( + private ArgumentResolverInterface $resolver, + private Stopwatch $stopwatch, + ) { } - /** - * @param \ReflectionFunctionAbstract|null $reflector - */ - public function getArguments(Request $request, callable $controller/* , \ReflectionFunctionAbstract $reflector = null */): array + public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { - $reflector = 2 < \func_num_args() ? func_get_arg(2) : null; $e = $this->stopwatch->start('controller.get_arguments'); try { diff --git a/Controller/TraceableControllerResolver.php b/Controller/TraceableControllerResolver.php index 60f29ab5db..f6b993c992 100644 --- a/Controller/TraceableControllerResolver.php +++ b/Controller/TraceableControllerResolver.php @@ -19,13 +19,10 @@ */ class TraceableControllerResolver implements ControllerResolverInterface { - private ControllerResolverInterface $resolver; - private Stopwatch $stopwatch; - - public function __construct(ControllerResolverInterface $resolver, Stopwatch $stopwatch) - { - $this->resolver = $resolver; - $this->stopwatch = $stopwatch; + public function __construct( + private ControllerResolverInterface $resolver, + private Stopwatch $stopwatch, + ) { } public function getController(Request $request): callable|false diff --git a/ControllerMetadata/ArgumentMetadata.php b/ControllerMetadata/ArgumentMetadata.php index dd6c8be86f..207fabc14f 100644 --- a/ControllerMetadata/ArgumentMetadata.php +++ b/ControllerMetadata/ArgumentMetadata.php @@ -20,26 +20,20 @@ class ArgumentMetadata { public const IS_INSTANCEOF = 2; - private string $name; - private ?string $type; - private bool $isVariadic; - private bool $hasDefaultValue; - private mixed $defaultValue; - private bool $isNullable; - private array $attributes; - /** * @param object[] $attributes */ - public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, mixed $defaultValue, bool $isNullable = false, array $attributes = []) - { - $this->name = $name; - $this->type = $type; - $this->isVariadic = $isVariadic; - $this->hasDefaultValue = $hasDefaultValue; - $this->defaultValue = $defaultValue; + public function __construct( + private string $name, + private ?string $type, + private bool $isVariadic, + private bool $hasDefaultValue, + private mixed $defaultValue, + private bool $isNullable = false, + private array $attributes = [], + private string $controllerName = 'n/a', + ) { $this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue); - $this->attributes = $attributes; } /** @@ -94,7 +88,7 @@ public function isNullable(): bool public function getDefaultValue(): mixed { if (!$this->hasDefaultValue) { - throw new \LogicException(sprintf('Argument $%s does not have a default value. Use "%s::hasDefaultValue()" to avoid this exception.', $this->name, __CLASS__)); + throw new \LogicException(\sprintf('Argument $%s does not have a default value. Use "%s::hasDefaultValue()" to avoid this exception.', $this->name, __CLASS__)); } return $this->defaultValue; @@ -142,4 +136,9 @@ public function getAttributesOfType(string $name, int $flags = 0): array return $attributes; } + + public function getControllerName(): string + { + return $this->controllerName; + } } diff --git a/ControllerMetadata/ArgumentMetadataFactory.php b/ControllerMetadata/ArgumentMetadataFactory.php index 7eafdc94b0..26b80f9dcf 100644 --- a/ControllerMetadata/ArgumentMetadataFactory.php +++ b/ControllerMetadata/ArgumentMetadataFactory.php @@ -22,6 +22,7 @@ public function createArgumentMetadata(string|object|array $controller, ?\Reflec { $arguments = []; $reflector ??= new \ReflectionFunction($controller(...)); + $controllerName = $this->getPrettyName($reflector); foreach ($reflector->getParameters() as $param) { $attributes = []; @@ -31,7 +32,7 @@ public function createArgumentMetadata(string|object|array $controller, ?\Reflec } } - $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes); + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes, $controllerName); } return $arguments; @@ -53,4 +54,19 @@ private function getType(\ReflectionParameter $parameter): ?string default => $name, }; } + + private function getPrettyName(\ReflectionFunctionAbstract $r): string + { + $name = $r->name; + + if ($r instanceof \ReflectionMethod) { + return $r->class.'::'.$name; + } + + if ($r->isAnonymous() || !$class = $r->getClosureCalledClass()) { + return $name; + } + + return $class->name.'::'.$name; + } } diff --git a/ControllerMetadata/ArgumentMetadataFactoryInterface.php b/ControllerMetadata/ArgumentMetadataFactoryInterface.php index 954f901ef2..4f4bc07866 100644 --- a/ControllerMetadata/ArgumentMetadataFactoryInterface.php +++ b/ControllerMetadata/ArgumentMetadataFactoryInterface.php @@ -19,9 +19,7 @@ interface ArgumentMetadataFactoryInterface { /** - * @param \ReflectionFunctionAbstract|null $reflector - * * @return ArgumentMetadata[] */ - public function createArgumentMetadata(string|object|array $controller/* , \ReflectionFunctionAbstract $reflector = null */): array; + public function createArgumentMetadata(string|object|array $controller, ?\ReflectionFunctionAbstract $reflector = null): array; } diff --git a/DataCollector/ConfigDataCollector.php b/DataCollector/ConfigDataCollector.php index f9ca5da1d6..cc8ff3ada9 100644 --- a/DataCollector/ConfigDataCollector.php +++ b/DataCollector/ConfigDataCollector.php @@ -30,12 +30,8 @@ class ConfigDataCollector extends DataCollector implements LateDataCollectorInte /** * Sets the Kernel associated with this Request. */ - public function setKernel(?KernelInterface $kernel = null): void + public function setKernel(KernelInterface $kernel): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/http-kernel', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } - $this->kernel = $kernel; } @@ -44,10 +40,12 @@ public function collect(Request $request, Response $response, ?\Throwable $excep $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE); $eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE); + $xdebugMode = getenv('XDEBUG_MODE') ?: \ini_get('xdebug.mode'); + $this->data = [ 'token' => $response->headers->get('X-Debug-Token'), 'symfony_version' => Kernel::VERSION, - 'symfony_minor_version' => sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), + 'symfony_minor_version' => \sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), 'symfony_lts' => 4 === Kernel::MINOR_VERSION, 'symfony_state' => $this->determineSymfonyState(), 'symfony_eom' => $eom->format('F Y'), @@ -59,8 +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', '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', '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', 'bundles' => [], 'sapi_name' => \PHP_SAPI, ]; @@ -196,6 +197,11 @@ public function hasXdebug(): bool return $this->data['xdebug_enabled']; } + public function getXdebugStatus(): string + { + return $this->data['xdebug_status']; + } + /** * Returns true if the function xdebug_info is available. */ @@ -212,6 +218,11 @@ public function hasApcu(): bool return $this->data['apcu_enabled']; } + public function getApcuStatus(): string + { + return $this->data['apcu_status']; + } + /** * Returns true if Zend OPcache is enabled. */ @@ -220,6 +231,11 @@ public function hasZendOpcache(): bool return $this->data['zend_opcache_enabled']; } + public function getZendOpcacheStatus(): string + { + return $this->data['zend_opcache_status']; + } + public function getBundles(): array|Data { return $this->data['bundles']; diff --git a/DataCollector/DataCollector.php b/DataCollector/DataCollector.php index fdc73de06f..3238e2bb8d 100644 --- a/DataCollector/DataCollector.php +++ b/DataCollector/DataCollector.php @@ -28,10 +28,7 @@ */ abstract class DataCollector implements DataCollectorInterface { - /** - * @var array|Data - */ - protected $data = []; + protected array|Data $data = []; private ClonerInterface $cloner; @@ -58,9 +55,9 @@ protected function cloneVar(mixed $var): Data /** * @return callable[] The casters to add to the cloner */ - protected function getCasters() + protected function getCasters(): array { - $casters = [ + return [ '*' => function ($v, array $a, Stub $s, $isNested) { if (!$v instanceof Stub) { $b = $a; @@ -85,8 +82,6 @@ protected function getCasters() return $a; }, ] + ReflectionCaster::UNSET_CLOSURE_FILE_INFO; - - return $casters; } public function __sleep(): array @@ -94,10 +89,7 @@ public function __sleep(): array return ['data']; } - /** - * @return void - */ - public function __wakeup() + public function __wakeup(): void { } diff --git a/DataCollector/DumpDataCollector.php b/DataCollector/DumpDataCollector.php index 0a46a8cd4e..f1d13d326f 100644 --- a/DataCollector/DumpDataCollector.php +++ b/DataCollector/DumpDataCollector.php @@ -31,7 +31,6 @@ */ class DumpDataCollector extends DataCollector implements DataDumperInterface { - private ?Stopwatch $stopwatch = null; private string|FileLinkFormatter|false $fileLinkFormat; private int $dataCount = 0; private bool $isCollected = true; @@ -39,19 +38,20 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface private int $clonesIndex = 0; private array $rootRefs; private string $charset; - private ?RequestStack $requestStack; - private DataDumperInterface|Connection|null $dumper; private mixed $sourceContextProvider; private bool $webMode; - public function __construct(?Stopwatch $stopwatch = null, string|FileLinkFormatter|null $fileLinkFormat = null, ?string $charset = null, ?RequestStack $requestStack = null, DataDumperInterface|Connection|null $dumper = null, ?bool $webMode = null) - { + public function __construct( + private ?Stopwatch $stopwatch = null, + string|FileLinkFormatter|null $fileLinkFormat = null, + ?string $charset = null, + private ?RequestStack $requestStack = null, + private DataDumperInterface|Connection|null $dumper = null, + ?bool $webMode = null, + ) { $fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); - $this->stopwatch = $stopwatch; $this->fileLinkFormat = $fileLinkFormat instanceof FileLinkFormatter && false === $fileLinkFormat->format('', 0) ? false : $fileLinkFormat; $this->charset = $charset ?: \ini_get('php.output_encoding') ?: \ini_get('default_charset') ?: 'UTF-8'; - $this->requestStack = $requestStack; - $this->dumper = $dumper; $this->webMode = $webMode ?? !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true); // All clones share these properties by reference: @@ -121,12 +121,12 @@ public function collect(Request $request, Response $response, ?\Throwable $excep ) { if ($response->headers->has('Content-Type') && str_contains($response->headers->get('Content-Type') ?? '', 'html')) { $dumper = new HtmlDumper('php://output', $this->charset); - $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } else { $dumper = new CliDumper('php://output', $this->charset); - $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } + $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); + foreach ($this->data as $dump) { $this->doDump($dumper, $dump['data'], $dump['name'], $dump['file'], $dump['line'], $dump['label'] ?? ''); } @@ -180,7 +180,7 @@ public function __wakeup(): void } } - self::__construct($this->stopwatch, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null); + self::__construct($this->stopwatch ?? null, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null); } public function getDumpsCount(): int @@ -196,7 +196,7 @@ public function getDumps(string $format, int $maxDepthLimit = -1, int $maxItemsP $dumper = new HtmlDumper($data, $this->charset); $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } else { - throw new \InvalidArgumentException(sprintf('Invalid dump format: "%s".', $format)); + throw new \InvalidArgumentException(\sprintf('Invalid dump format: "%s".', $format)); } $dumps = []; @@ -235,12 +235,12 @@ public function __destruct() if ($this->webMode) { $dumper = new HtmlDumper('php://output', $this->charset); - $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } else { $dumper = new CliDumper('php://output', $this->charset); - $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } + $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); + foreach ($this->data as $i => $dump) { $this->data[$i] = null; $this->doDump($dumper, $dump['data'], $dump['name'], $dump['file'], $dump['line'], $dump['label'] ?? ''); @@ -263,9 +263,9 @@ private function doDump(DataDumperInterface $dumper, Data $data, string $name, s $f = strip_tags($this->style('', $file)); $name = strip_tags($this->style('', $name)); if ($fmt && $link = \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line)) { - $name = sprintf(''.$s.'', strip_tags($this->style('', $link)), $f, $name); + $name = \sprintf(''.$s.'', strip_tags($this->style('', $link)), $f, $name); } else { - $name = sprintf(''.$s.'', $f, $name); + $name = \sprintf(''.$s.'', $f, $name); } } else { $name = $this->style('meta', $name); diff --git a/DataCollector/LoggerDataCollector.php b/DataCollector/LoggerDataCollector.php index cf17e7a739..29024f6e74 100644 --- a/DataCollector/LoggerDataCollector.php +++ b/DataCollector/LoggerDataCollector.php @@ -27,16 +27,15 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface { private ?DebugLoggerInterface $logger; - private ?string $containerPathPrefix; private ?Request $currentRequest = null; - private ?RequestStack $requestStack; private ?array $processedLogs = null; - public function __construct(?object $logger = null, ?string $containerPathPrefix = null, ?RequestStack $requestStack = null) - { + public function __construct( + ?object $logger = null, + private ?string $containerPathPrefix = null, + private ?RequestStack $requestStack = null, + ) { $this->logger = DebugLoggerConfigurator::getDebugLogger($logger); - $this->containerPathPrefix = $containerPathPrefix; - $this->requestStack = $requestStack; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void @@ -192,7 +191,7 @@ private function getContainerDeprecationLogs(): array $log['priorityName'] = 'DEBUG'; $log['channel'] = null; $log['scream'] = false; - unset($log['type'], $log['file'], $log['line'], $log['trace'], $log['trace'], $log['count']); + unset($log['type'], $log['file'], $log['line'], $log['trace'], $log['count']); $logs[] = $log; } @@ -234,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 + [ @@ -313,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']; diff --git a/DataCollector/RequestDataCollector.php b/DataCollector/RequestDataCollector.php index 12951b495c..235bd4c74f 100644 --- a/DataCollector/RequestDataCollector.php +++ b/DataCollector/RequestDataCollector.php @@ -36,12 +36,11 @@ class RequestDataCollector extends DataCollector implements EventSubscriberInter */ private \SplObjectStorage $controllers; private array $sessionUsages = []; - private ?RequestStack $requestStack; - public function __construct(?RequestStack $requestStack = null) - { + public function __construct( + private ?RequestStack $requestStack = null, + ) { $this->controllers = new \SplObjectStorage(); - $this->requestStack = $requestStack; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void @@ -195,74 +194,47 @@ public function getPathInfo(): string return $this->data['path_info']; } - /** - * @return ParameterBag - */ - public function getRequestRequest() + public function getRequestRequest(): ParameterBag { return new ParameterBag($this->data['request_request']->getValue()); } - /** - * @return ParameterBag - */ - public function getRequestQuery() + public function getRequestQuery(): ParameterBag { return new ParameterBag($this->data['request_query']->getValue()); } - /** - * @return ParameterBag - */ - public function getRequestFiles() + public function getRequestFiles(): ParameterBag { return new ParameterBag($this->data['request_files']->getValue()); } - /** - * @return ParameterBag - */ - public function getRequestHeaders() + public function getRequestHeaders(): ParameterBag { return new ParameterBag($this->data['request_headers']->getValue()); } - /** - * @return ParameterBag - */ - public function getRequestServer(bool $raw = false) + public function getRequestServer(bool $raw = false): ParameterBag { return new ParameterBag($this->data['request_server']->getValue($raw)); } - /** - * @return ParameterBag - */ - public function getRequestCookies(bool $raw = false) + public function getRequestCookies(bool $raw = false): ParameterBag { return new ParameterBag($this->data['request_cookies']->getValue($raw)); } - /** - * @return ParameterBag - */ - public function getRequestAttributes() + public function getRequestAttributes(): ParameterBag { return new ParameterBag($this->data['request_attributes']->getValue()); } - /** - * @return ParameterBag - */ - public function getResponseHeaders() + public function getResponseHeaders(): ParameterBag { return new ParameterBag($this->data['response_headers']->getValue()); } - /** - * @return ParameterBag - */ - public function getResponseCookies() + public function getResponseCookies(): ParameterBag { return new ParameterBag($this->data['response_cookies']->getValue()); } @@ -300,18 +272,12 @@ public function getContent() return $this->data['content']; } - /** - * @return bool - */ - public function isJsonRequest() + public function isJsonRequest(): bool { return 1 === preg_match('{^application/(?:\w+\++)*json$}i', $this->data['request_headers']['content-type']); } - /** - * @return string|null - */ - public function getPrettyJson() + public function getPrettyJson(): ?string { $decoded = json_decode($this->getContent()); @@ -343,10 +309,7 @@ public function getLocale(): string return $this->data['locale']; } - /** - * @return ParameterBag - */ - public function getDotenvVars() + public function getDotenvVars(): ParameterBag { return new ParameterBag($this->data['dotenv_vars']->getValue()); } @@ -452,7 +415,7 @@ public function collectSessionUsage(): void array_splice($trace, 0, $traceEndIndex); // Merge identical backtraces generated by internal call reports - $name = sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']); + $name = \sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']); if (!\array_key_exists($name, $this->sessionUsages)) { $this->sessionUsages[$name] = [ 'name' => $name, @@ -505,12 +468,12 @@ private function parseController(array|object|string|null $controller): array|st 'line' => $r->getStartLine(), ]; - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { return $controller; } $controller['method'] = $r->name; - if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + if ($class = $r->getClosureCalledClass()) { $controller['class'] = $class->name; } else { return $r->name; diff --git a/DataCollector/RouterDataCollector.php b/DataCollector/RouterDataCollector.php index 4d91fd6e14..cb7b233f5d 100644 --- a/DataCollector/RouterDataCollector.php +++ b/DataCollector/RouterDataCollector.php @@ -24,7 +24,7 @@ class RouterDataCollector extends DataCollector /** * @var \SplObjectStorage */ - protected $controllers; + protected \SplObjectStorage $controllers; public function __construct() { @@ -48,10 +48,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep unset($this->controllers[$request]); } - /** - * @return void - */ - public function reset() + public function reset(): void { $this->controllers = new \SplObjectStorage(); @@ -62,20 +59,15 @@ public function reset() ]; } - /** - * @return string - */ - protected function guessRoute(Request $request, string|object|array $controller) + protected function guessRoute(Request $request, string|object|array $controller): string { return 'n/a'; } /** * Remembers the controller associated to each request. - * - * @return void */ - public function onKernelController(ControllerEvent $event) + public function onKernelController(ControllerEvent $event): void { $this->controllers[$event->getRequest()] = $event->getController(); } diff --git a/DataCollector/TimeDataCollector.php b/DataCollector/TimeDataCollector.php index 9799a1333d..c9b830ff03 100644 --- a/DataCollector/TimeDataCollector.php +++ b/DataCollector/TimeDataCollector.php @@ -24,13 +24,10 @@ */ class TimeDataCollector extends DataCollector implements LateDataCollectorInterface { - private ?KernelInterface $kernel; - private ?Stopwatch $stopwatch; - - public function __construct(?KernelInterface $kernel = null, ?Stopwatch $stopwatch = null) - { - $this->kernel = $kernel; - $this->stopwatch = $stopwatch; + public function __construct( + private readonly ?KernelInterface $kernel = null, + private readonly ?Stopwatch $stopwatch = null, + ) { $this->data = ['events' => [], 'stopwatch_installed' => false, 'start_time' => 0]; } diff --git a/Debug/ErrorHandlerConfigurator.php b/Debug/ErrorHandlerConfigurator.php index 5b3e1cdddf..38650bebb5 100644 --- a/Debug/ErrorHandlerConfigurator.php +++ b/Debug/ErrorHandlerConfigurator.php @@ -23,12 +23,8 @@ */ class ErrorHandlerConfigurator { - private ?LoggerInterface $logger; - private ?LoggerInterface $deprecationLogger; private array|int|null $levels; private ?int $throwAt; - private bool $scream; - private bool $scope; /** * @param array|int|null $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants @@ -36,14 +32,16 @@ class ErrorHandlerConfigurator * @param bool $scream Enables/disables screaming mode, where even silenced errors are logged * @param bool $scope Enables/disables scoping mode */ - public function __construct(?LoggerInterface $logger = null, array|int|null $levels = \E_ALL, ?int $throwAt = \E_ALL, bool $scream = true, bool $scope = true, ?LoggerInterface $deprecationLogger = null) - { - $this->logger = $logger; + public function __construct( + private ?LoggerInterface $logger = null, + array|int|null $levels = \E_ALL, + ?int $throwAt = \E_ALL, + private bool $scream = true, + private bool $scope = true, + private ?LoggerInterface $deprecationLogger = null, + ) { $this->levels = $levels ?? \E_ALL; $this->throwAt = \is_int($throwAt) ? $throwAt : (null === $throwAt ? null : ($throwAt ? \E_ALL : null)); - $this->scream = $scream; - $this->scope = $scope; - $this->deprecationLogger = $deprecationLogger; } /** diff --git a/Debug/FileLinkFormatter.php b/Debug/FileLinkFormatter.php deleted file mode 100644 index 600a460fb6..0000000000 --- a/Debug/FileLinkFormatter.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\Debug; - -use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter as ErrorHandlerFileLinkFormatter; - -trigger_deprecation('symfony/http-kernel', '6.4', 'The "%s" class is deprecated, use "%s" instead.', FileLinkFormatter::class, ErrorHandlerFileLinkFormatter::class); - -class_exists(ErrorHandlerFileLinkFormatter::class); - -if (!class_exists(FileLinkFormatter::class, false)) { - class_alias(ErrorHandlerFileLinkFormatter::class, FileLinkFormatter::class); -} - -if (false) { - /** - * @deprecated since Symfony 6.4, use FileLinkFormatter from the ErrorHandler component instead - */ - class FileLinkFormatter extends ErrorHandlerFileLinkFormatter - { - } -} diff --git a/Debug/TraceableEventDispatcher.php b/Debug/TraceableEventDispatcher.php index f3101d5b14..915862eddb 100644 --- a/Debug/TraceableEventDispatcher.php +++ b/Debug/TraceableEventDispatcher.php @@ -25,9 +25,12 @@ 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', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $event->getRequest()->attributes->set('_stopwatch_token', bin2hex(random_bytes(3))); $this->stopwatch->openSection(); break; case KernelEvents::VIEW: @@ -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/DependencyInjection/AddAnnotatedClassesToCachePass.php b/DependencyInjection/AddAnnotatedClassesToCachePass.php index 1924b1ddb0..c8ed6b8e41 100644 --- a/DependencyInjection/AddAnnotatedClassesToCachePass.php +++ b/DependencyInjection/AddAnnotatedClassesToCachePass.php @@ -17,24 +17,23 @@ use Symfony\Component\ErrorHandler\DebugClassLoader; use Symfony\Component\HttpKernel\Kernel; +trigger_deprecation('symfony/http-kernel', '7.1', 'The "%s" class is deprecated since Symfony 7.1 and will be removed in 8.0.', AddAnnotatedClassesToCachePass::class); + /** * Sets the classes to compile in the cache for the container. * * @author Fabien Potencier + * + * @deprecated since Symfony 7.1, to be removed in 8.0 */ class AddAnnotatedClassesToCachePass implements CompilerPassInterface { - private Kernel $kernel; - - public function __construct(Kernel $kernel) - { - $this->kernel = $kernel; + public function __construct( + private Kernel $kernel, + ) { } - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { $annotatedClasses = []; foreach ($container->getExtensions() as $extension) { diff --git a/DependencyInjection/ConfigurableExtension.php b/DependencyInjection/ConfigurableExtension.php index 12d468cf04..714fdb7195 100644 --- a/DependencyInjection/ConfigurableExtension.php +++ b/DependencyInjection/ConfigurableExtension.php @@ -34,8 +34,6 @@ final public function load(array $configs, ContainerBuilder $container): void /** * Configures the passed container according to the merged configuration. - * - * @return void */ - abstract protected function loadInternal(array $mergedConfig, ContainerBuilder $container); + abstract protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void; } diff --git a/DependencyInjection/ControllerArgumentValueResolverPass.php b/DependencyInjection/ControllerArgumentValueResolverPass.php index d3b157418e..d760e3bcc1 100644 --- a/DependencyInjection/ControllerArgumentValueResolverPass.php +++ b/DependencyInjection/ControllerArgumentValueResolverPass.php @@ -30,10 +30,7 @@ class ControllerArgumentValueResolverPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('argument_resolver')) { return; diff --git a/DependencyInjection/Extension.php b/DependencyInjection/Extension.php index d72efa1724..87b81a8c5e 100644 --- a/DependencyInjection/Extension.php +++ b/DependencyInjection/Extension.php @@ -17,6 +17,8 @@ * Allow adding classes to the class cache. * * @author Fabien Potencier + * + * @internal since Symfony 7.1, to be deprecated in 8.1; use Symfony\Component\DependencyInjection\Extension\Extension instead */ abstract class Extension extends BaseExtension { @@ -24,21 +26,29 @@ abstract class Extension extends BaseExtension /** * Gets the annotated classes to cache. + * + * @return string[] + * + * @deprecated since Symfony 7.1, to be removed in 8.0 */ public function getAnnotatedClassesToCompile(): array { + trigger_deprecation('symfony/http-kernel', '7.1', 'The "%s()" method is deprecated since Symfony 7.1 and will be removed in 8.0.', __METHOD__); + return $this->annotatedClasses; } /** * Adds annotated classes to the class cache. * - * @param array $annotatedClasses An array of class patterns + * @param string[] $annotatedClasses An array of class patterns * - * @return void + * @deprecated since Symfony 7.1, to be removed in 8.0 */ - public function addAnnotatedClassesToCompile(array $annotatedClasses) + public function addAnnotatedClassesToCompile(array $annotatedClasses): void { + trigger_deprecation('symfony/http-kernel', '7.1', 'The "%s()" method is deprecated since Symfony 7.1 and will be removed in 8.0.', __METHOD__); + $this->annotatedClasses = array_merge($this->annotatedClasses, $annotatedClasses); } } diff --git a/DependencyInjection/FragmentRendererPass.php b/DependencyInjection/FragmentRendererPass.php index f41d58b81b..2ac044f1a3 100644 --- a/DependencyInjection/FragmentRendererPass.php +++ b/DependencyInjection/FragmentRendererPass.php @@ -25,10 +25,7 @@ */ class FragmentRendererPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('fragment.handler')) { return; @@ -41,10 +38,10 @@ public function process(ContainerBuilder $container) $class = $container->getParameterBag()->resolveValue($def->getClass()); if (!$r = $container->getReflectionClass($class)) { - throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(FragmentRendererInterface::class)) { - throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, FragmentRendererInterface::class)); + throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, FragmentRendererInterface::class)); } foreach ($tags as $tag) { diff --git a/DependencyInjection/LazyLoadingFragmentHandler.php b/DependencyInjection/LazyLoadingFragmentHandler.php index 944b5d0bc7..d8ff64bcc2 100644 --- a/DependencyInjection/LazyLoadingFragmentHandler.php +++ b/DependencyInjection/LazyLoadingFragmentHandler.php @@ -23,17 +23,16 @@ */ class LazyLoadingFragmentHandler extends FragmentHandler { - private ContainerInterface $container; - /** * @var array */ private array $initialized = []; - public function __construct(ContainerInterface $container, RequestStack $requestStack, bool $debug = false) - { - $this->container = $container; - + public function __construct( + private ContainerInterface $container, + RequestStack $requestStack, + bool $debug = false, + ) { parent::__construct($requestStack, [], $debug); } diff --git a/DependencyInjection/LoggerPass.php b/DependencyInjection/LoggerPass.php index 0061a577c7..7566d7358c 100644 --- a/DependencyInjection/LoggerPass.php +++ b/DependencyInjection/LoggerPass.php @@ -25,10 +25,7 @@ */ class LoggerPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->has(LoggerInterface::class)) { $container->setAlias(LoggerInterface::class, 'logger'); diff --git a/DependencyInjection/MergeExtensionConfigurationPass.php b/DependencyInjection/MergeExtensionConfigurationPass.php index d65dbbab03..8336d8c297 100644 --- a/DependencyInjection/MergeExtensionConfigurationPass.php +++ b/DependencyInjection/MergeExtensionConfigurationPass.php @@ -21,14 +21,12 @@ */ class MergeExtensionConfigurationPass extends BaseMergeExtensionConfigurationPass { - private array $extensions; - /** * @param string[] $extensions */ - public function __construct(array $extensions) - { - $this->extensions = $extensions; + public function __construct( + private array $extensions, + ) { } public function process(ContainerBuilder $container): void diff --git a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index 7d13c223a6..a5a863cc15 100644 --- a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -34,10 +34,7 @@ */ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('argument_resolver.service') && !$container->hasDefinition('argument_resolver.not_tagged_controller')) { return; @@ -54,8 +51,6 @@ public function process(ContainerBuilder $container) } } - $emptyAutowireAttributes = class_exists(Autowire::class) ? null : []; - foreach ($container->findTaggedServiceIds('controller.service_arguments', true) as $id => $tags) { $def = $container->getDefinition($id); $def->setPublic(true); @@ -73,7 +68,7 @@ public function process(ContainerBuilder $container) $class = $parameterBag->resolveValue($class); if (!$r = $container->getReflectionClass($class)) { - throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } $controllerClasses[] = $class; @@ -98,11 +93,11 @@ public function process(ContainerBuilder $container) } foreach (['action', 'argument', 'id'] as $k) { if (!isset($attributes[$k][0])) { - throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "controller.service_arguments" %s for service "%s".', $k, json_encode($attributes, \JSON_UNESCAPED_UNICODE), $id)); + throw new InvalidArgumentException(\sprintf('Missing "%s" attribute on tag "controller.service_arguments" %s for service "%s".', $k, json_encode($attributes, \JSON_UNESCAPED_UNICODE), $id)); } } if (!isset($methods[$action = strtolower($attributes['action'])])) { - throw new InvalidArgumentException(sprintf('Invalid "action" attribute on tag "controller.service_arguments" for service "%s": no public "%s()" method found on class "%s".', $id, $attributes['action'], $class)); + throw new InvalidArgumentException(\sprintf('Invalid "action" attribute on tag "controller.service_arguments" for service "%s": no public "%s()" method found on class "%s".', $id, $attributes['action'], $class)); } [$r, $parameters] = $methods[$action]; $found = false; @@ -118,7 +113,7 @@ public function process(ContainerBuilder $container) } if (!$found) { - throw new InvalidArgumentException(sprintf('Invalid "controller.service_arguments" tag for service "%s": method "%s()" has no "%s" argument on class "%s".', $id, $r->name, $attributes['argument'], $class)); + throw new InvalidArgumentException(\sprintf('Invalid "controller.service_arguments" tag for service "%s": method "%s()" has no "%s" argument on class "%s".', $id, $r->name, $attributes['argument'], $class)); } } @@ -127,11 +122,12 @@ public function process(ContainerBuilder $container) // create a per-method map of argument-names to service/type-references $args = []; + $erroredIds = 0; foreach ($parameters as $p) { /** @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; @@ -139,8 +135,8 @@ public function process(ContainerBuilder $container) $target = $arguments[$r->name][$p->name]; if ('?' !== $target[0]) { $invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE; - } elseif ('' === $target = (string) substr($target, 1)) { - throw new InvalidArgumentException(sprintf('A "controller.service_arguments" tag must have non-empty "id" attributes for service "%s".', $id)); + } elseif ('' === $target = substr($target, 1)) { + throw new InvalidArgumentException(\sprintf('A "controller.service_arguments" tag must have non-empty "id" attributes for service "%s".', $id)); } elseif ($p->allowsNull() && !$p->isOptional()) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; } @@ -157,7 +153,7 @@ public function process(ContainerBuilder $container) $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 (!$autowireAttributes && is_subclass_of($type, \UnitEnum::class)) { // do not attempt to register enum typed arguments if not already present in bindings @@ -175,10 +171,8 @@ public function process(ContainerBuilder $container) $value = $parameterBag->resolveValue($attribute->value); if ($attribute instanceof AutowireCallable) { - $value = $attribute->buildDefinition($value, $type, $p); - } - - if ($value instanceof Reference) { + $args[$p->name] = $attribute->buildDefinition($value, $type, $p); + } elseif ($value instanceof Reference) { $args[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior); } else { $args[$p->name] = new Reference('.value.'.$container->hash($value)); @@ -191,7 +185,7 @@ public function process(ContainerBuilder $container) } if ($type && !$p->isOptional() && !$p->allowsNull() && !class_exists($type) && !interface_exists($type, false)) { - $message = sprintf('Cannot determine controller argument for "%s::%s()": the $%s argument is type-hinted with the non-existent class or interface: "%s".', $class, $r->name, $p->name, $type); + $message = \sprintf('Cannot determine controller argument for "%s::%s()": the $%s argument is type-hinted with the non-existent class or interface: "%s".', $class, $r->name, $p->name, $type); // see if the type-hint lives in the same namespace as the controller if (0 === strncmp($type, $class, strrpos($class, '\\'))) { @@ -202,6 +196,7 @@ public function process(ContainerBuilder $container) ->addError($message); $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); + ++$erroredIds; } else { $target = preg_replace('/(^|[(|&])\\\\/', '\1', $target); $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior); @@ -209,7 +204,7 @@ public function process(ContainerBuilder $container) } // register the maps as a per-method service-locators if ($args) { - $controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args); + $controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args, \count($args) !== $erroredIds ? $id.'::'.$r->name.'()' : null); foreach ($publicAliases[$id] ?? [] as $alias) { $controllers[$alias.'::'.$r->name] = clone $controllers[$id.'::'.$r->name]; diff --git a/DependencyInjection/RegisterLocaleAwareServicesPass.php b/DependencyInjection/RegisterLocaleAwareServicesPass.php index 2a01365bd3..3c7d5ac3c9 100644 --- a/DependencyInjection/RegisterLocaleAwareServicesPass.php +++ b/DependencyInjection/RegisterLocaleAwareServicesPass.php @@ -23,10 +23,7 @@ */ class RegisterLocaleAwareServicesPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('locale_aware_listener')) { return; diff --git a/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php b/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php index 7a21fe0e59..809b9c6b36 100644 --- a/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php @@ -21,10 +21,7 @@ */ class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { $controllerLocator = $container->findDefinition('argument_resolver.controller_locator'); $controllers = $controllerLocator->getArgument(0); @@ -32,9 +29,13 @@ public function process(ContainerBuilder $container) foreach ($controllers as $controller => $argumentRef) { $argumentLocator = $container->getDefinition((string) $argumentRef->getValues()[0]); + if ($argumentLocator->getFactory()) { + $argumentLocator = $container->getDefinition($argumentLocator->getFactory()[0]); + } + if (!$argumentLocator->getArgument(0)) { // remove empty argument locators - $reason = sprintf('Removing service-argument resolver for controller "%s": no corresponding services exist for the referenced types.', $controller); + $reason = \sprintf('Removing service-argument resolver for controller "%s": no corresponding services exist for the referenced types.', $controller); } else { // any methods listed for call-at-instantiation cannot be actions $reason = false; @@ -47,7 +48,7 @@ public function process(ContainerBuilder $container) $controllerDef = $container->getDefinition($id); foreach ($controllerDef->getMethodCalls() as [$method]) { if (0 === strcasecmp($action, $method)) { - $reason = sprintf('Removing method "%s" of service "%s" from controller candidates: the method is called at instantiation, thus cannot be an action.', $action, $id); + $reason = \sprintf('Removing method "%s" of service "%s" from controller candidates: the method is called at instantiation, thus cannot be an action.', $action, $id); break; } } diff --git a/DependencyInjection/ResettableServicePass.php b/DependencyInjection/ResettableServicePass.php index da9f8d6320..1789d8bcd6 100644 --- a/DependencyInjection/ResettableServicePass.php +++ b/DependencyInjection/ResettableServicePass.php @@ -23,10 +23,7 @@ */ class ResettableServicePass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (!$container->has('services_resetter')) { return; @@ -39,7 +36,7 @@ public function process(ContainerBuilder $container) foreach ($tags as $attributes) { if (!isset($attributes['method'])) { - throw new RuntimeException(sprintf('Tag "kernel.reset" requires the "method" attribute to be set on service "%s".', $id)); + throw new RuntimeException(\sprintf('Tag "kernel.reset" requires the "method" attribute to be set on service "%s".', $id)); } if (!isset($methods[$id])) { diff --git a/DependencyInjection/ServicesResetter.php b/DependencyInjection/ServicesResetter.php index b38ab5658d..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. @@ -21,21 +20,18 @@ * @author Alexander M. Turek * @author Nicolas Grekas * - * @internal + * @final since Symfony 7.2 */ -class ServicesResetter implements ResetInterface +class ServicesResetter implements ServicesResetterInterface { - private \Traversable $resettableServices; - private array $resetMethods; - /** * @param \Traversable $resettableServices * @param array $resetMethods */ - public function __construct(\Traversable $resettableServices, array $resetMethods) - { - $this->resettableServices = $resettableServices; - $this->resetMethods = $resetMethods; + public function __construct( + private \Traversable $resettableServices, + private array $resetMethods, + ) { } public function reset(): void @@ -49,6 +45,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; 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 +{ +} diff --git a/Event/ControllerArgumentsEvent.php b/Event/ControllerArgumentsEvent.php index 4c804ccf19..f0273fd099 100644 --- a/Event/ControllerArgumentsEvent.php +++ b/Event/ControllerArgumentsEvent.php @@ -29,11 +29,15 @@ final class ControllerArgumentsEvent extends KernelEvent { private ControllerEvent $controllerEvent; - private array $arguments; private array $namedArguments; - public function __construct(HttpKernelInterface $kernel, callable|ControllerEvent $controller, array $arguments, Request $request, ?int $requestType) - { + public function __construct( + HttpKernelInterface $kernel, + callable|ControllerEvent $controller, + private array $arguments, + Request $request, + ?int $requestType, + ) { parent::__construct($kernel, $request, $requestType); if (!$controller instanceof ControllerEvent) { @@ -41,7 +45,6 @@ public function __construct(HttpKernelInterface $kernel, callable|ControllerEven } $this->controllerEvent = $controller; - $this->arguments = $arguments; } public function getController(): callable diff --git a/Event/ControllerEvent.php b/Event/ControllerEvent.php index 6db2c15f9e..3d03e6de0d 100644 --- a/Event/ControllerEvent.php +++ b/Event/ControllerEvent.php @@ -98,7 +98,7 @@ public function getAttributes(?string $className = null): array } elseif (\is_string($this->controller) && false !== $i = strpos($this->controller, '::')) { $class = new \ReflectionClass(substr($this->controller, 0, $i)); } else { - $class = str_contains($this->controllerReflector->name, '{closure') ? null : (\PHP_VERSION_ID >= 80111 ? $this->controllerReflector->getClosureCalledClass() : $this->controllerReflector->getClosureScopeClass()); + $class = $this->controllerReflector instanceof \ReflectionFunction && $this->controllerReflector->isAnonymous() ? null : $this->controllerReflector->getClosureCalledClass(); } $this->attributes = []; diff --git a/Event/ExceptionEvent.php b/Event/ExceptionEvent.php index 8bc25f9c37..d4f4c4ddfd 100644 --- a/Event/ExceptionEvent.php +++ b/Event/ExceptionEvent.php @@ -32,8 +32,13 @@ final class ExceptionEvent extends RequestEvent private \Throwable $throwable; private bool $allowCustomResponseCode = false; - public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, \Throwable $e) - { + public function __construct( + HttpKernelInterface $kernel, + Request $request, + int $requestType, + \Throwable $e, + private bool $isKernelTerminating = false, + ) { parent::__construct($kernel, $request, $requestType); $this->setThrowable($e); @@ -69,4 +74,9 @@ public function isAllowingCustomResponseCode(): bool { return $this->allowCustomResponseCode; } + + public function isKernelTerminating(): bool + { + return $this->isKernelTerminating; + } } diff --git a/Event/KernelEvent.php b/Event/KernelEvent.php index 02426c52a1..bc6643f0a4 100644 --- a/Event/KernelEvent.php +++ b/Event/KernelEvent.php @@ -22,19 +22,15 @@ */ class KernelEvent extends Event { - private HttpKernelInterface $kernel; - private Request $request; - private ?int $requestType; - /** * @param int $requestType The request type the kernel is currently processing; one of * HttpKernelInterface::MAIN_REQUEST or HttpKernelInterface::SUB_REQUEST */ - public function __construct(HttpKernelInterface $kernel, Request $request, ?int $requestType) - { - $this->kernel = $kernel; - $this->request = $request; - $this->requestType = $requestType; + public function __construct( + private HttpKernelInterface $kernel, + private Request $request, + private ?int $requestType, + ) { } /** diff --git a/Event/RequestEvent.php b/Event/RequestEvent.php index b81a79b780..2ca242b734 100644 --- a/Event/RequestEvent.php +++ b/Event/RequestEvent.php @@ -36,10 +36,8 @@ public function getResponse(): ?Response /** * Sets a response and stops event propagation. - * - * @return void */ - public function setResponse(Response $response) + public function setResponse(Response $response): void { $this->response = $response; @@ -48,6 +46,8 @@ public function setResponse(Response $response) /** * Returns whether a response was set. + * + * @psalm-assert-if-true !null $this->getResponse() */ public function hasResponse(): bool { diff --git a/Event/ResponseEvent.php b/Event/ResponseEvent.php index 4a57d989ac..65235d0bcb 100644 --- a/Event/ResponseEvent.php +++ b/Event/ResponseEvent.php @@ -26,13 +26,13 @@ */ final class ResponseEvent extends KernelEvent { - private Response $response; - - public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, Response $response) - { + public function __construct( + HttpKernelInterface $kernel, + Request $request, + int $requestType, + private Response $response, + ) { parent::__construct($kernel, $request, $requestType); - - $this->setResponse($response); } public function getResponse(): Response diff --git a/Event/TerminateEvent.php b/Event/TerminateEvent.php index 0caefdf4d5..95a0bc81f0 100644 --- a/Event/TerminateEvent.php +++ b/Event/TerminateEvent.php @@ -25,13 +25,12 @@ */ final class TerminateEvent extends KernelEvent { - private Response $response; - - public function __construct(HttpKernelInterface $kernel, Request $request, Response $response) - { + public function __construct( + HttpKernelInterface $kernel, + Request $request, + private Response $response, + ) { parent::__construct($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - - $this->response = $response; } public function getResponse(): Response diff --git a/Event/ViewEvent.php b/Event/ViewEvent.php index 4d963aea1f..5f466b5814 100644 --- a/Event/ViewEvent.php +++ b/Event/ViewEvent.php @@ -25,15 +25,14 @@ */ final class ViewEvent extends RequestEvent { - public readonly ?ControllerArgumentsEvent $controllerArgumentsEvent; - private mixed $controllerResult; - - public function __construct(HttpKernelInterface $kernel, Request $request, int $requestType, mixed $controllerResult, ?ControllerArgumentsEvent $controllerArgumentsEvent = null) - { + public function __construct( + HttpKernelInterface $kernel, + Request $request, + int $requestType, + private mixed $controllerResult, + public readonly ?ControllerArgumentsEvent $controllerArgumentsEvent = null, + ) { parent::__construct($kernel, $request, $requestType); - - $this->controllerResult = $controllerResult; - $this->controllerArgumentsEvent = $controllerArgumentsEvent; } public function getControllerResult(): mixed diff --git a/EventListener/AbstractSessionListener.php b/EventListener/AbstractSessionListener.php index 1ce4905376..74534aa8ac 100644 --- a/EventListener/AbstractSessionListener.php +++ b/EventListener/AbstractSessionListener.php @@ -41,25 +41,15 @@ abstract class AbstractSessionListener implements EventSubscriberInterface, Rese public const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl'; /** + * @param array $sessionOptions + * * @internal */ - protected ?ContainerInterface $container; - - private bool $debug; - - /** - * @var array - */ - private array $sessionOptions; - - /** - * @internal - */ - public function __construct(?ContainerInterface $container = null, bool $debug = false, array $sessionOptions = []) - { - $this->container = $container; - $this->debug = $debug; - $this->sessionOptions = $sessionOptions; + public function __construct( + private ?ContainerInterface $container = null, + private bool $debug = false, + private array $sessionOptions = [], + ) { } /** @@ -103,7 +93,7 @@ public function onKernelRequest(RequestEvent $event): void */ public function onKernelResponse(ResponseEvent $event): void { - if (!$event->isMainRequest() || (!$this->container->has('initialized_session') && !$event->getRequest()->hasSession())) { + if (!$event->isMainRequest()) { return; } diff --git a/EventListener/AddRequestFormatsListener.php b/EventListener/AddRequestFormatsListener.php index d4ef3fe498..f5dcdfe011 100644 --- a/EventListener/AddRequestFormatsListener.php +++ b/EventListener/AddRequestFormatsListener.php @@ -24,11 +24,9 @@ */ class AddRequestFormatsListener implements EventSubscriberInterface { - private array $formats; - - public function __construct(array $formats) - { - $this->formats = $formats; + public function __construct( + private array $formats, + ) { } /** diff --git a/EventListener/CacheAttributeListener.php b/EventListener/CacheAttributeListener.php index 723e758cd0..436e031bbb 100644 --- a/EventListener/CacheAttributeListener.php +++ b/EventListener/CacheAttributeListener.php @@ -46,10 +46,8 @@ public function __construct( /** * Handles HTTP validation headers. - * - * @return void */ - public function onKernelControllerArguments(ControllerArgumentsEvent $event) + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { $request = $event->getRequest(); @@ -92,10 +90,8 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event) /** * Modifies the response to apply HTTP cache headers when needed. - * - * @return void */ - public function onKernelResponse(ResponseEvent $event) + public function onKernelResponse(ResponseEvent $event): void { $request = $event->getRequest(); @@ -167,6 +163,14 @@ public function onKernelResponse(ResponseEvent $event) if (false === $cache->public) { $response->setPrivate(); } + + if (true === $cache->noStore) { + $response->headers->addCacheControlDirective('no-store'); + } + + if (false === $cache->noStore) { + $response->headers->removeCacheControlDirective('no-store'); + } } } diff --git a/EventListener/DumpListener.php b/EventListener/DumpListener.php index 07a4e7e6a0..836e54dcc3 100644 --- a/EventListener/DumpListener.php +++ b/EventListener/DumpListener.php @@ -25,21 +25,14 @@ */ class DumpListener implements EventSubscriberInterface { - private ClonerInterface $cloner; - private DataDumperInterface $dumper; - private ?Connection $connection; - - public function __construct(ClonerInterface $cloner, DataDumperInterface $dumper, ?Connection $connection = null) - { - $this->cloner = $cloner; - $this->dumper = $dumper; - $this->connection = $connection; + public function __construct( + private ClonerInterface $cloner, + private DataDumperInterface $dumper, + private ?Connection $connection = null, + ) { } - /** - * @return void - */ - public function configure() + public function configure(): void { $cloner = $this->cloner; $dumper = $this->dumper; diff --git a/EventListener/ErrorListener.php b/EventListener/ErrorListener.php index 7aa4875e5a..81f5dfb7fc 100644 --- a/EventListener/ErrorListener.php +++ b/EventListener/ErrorListener.php @@ -33,32 +33,23 @@ */ class ErrorListener implements EventSubscriberInterface { - protected $controller; - protected $logger; - protected $debug; /** - * @var array|null}> + * @param array|null, log_channel: string|null}> $exceptionsMapping */ - protected $exceptionsMapping; - - /** - * @param array|null}> $exceptionsMapping - */ - public function __construct(string|object|array|null $controller, ?LoggerInterface $logger = null, bool $debug = false, array $exceptionsMapping = []) - { - $this->controller = $controller; - $this->logger = $logger; - $this->debug = $debug; - $this->exceptionsMapping = $exceptionsMapping; + public function __construct( + protected string|object|array|null $controller, + protected ?LoggerInterface $logger = null, + protected bool $debug = false, + protected array $exceptionsMapping = [], + protected array $loggers = [], + ) { } - /** - * @return void - */ - public function logKernelException(ExceptionEvent $event) + 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']) { @@ -66,42 +57,33 @@ public function logKernelException(ExceptionEvent $event) } if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() !== $config['status_code']) { $headers = $throwable instanceof HttpExceptionInterface ? $throwable->getHeaders() : []; - $throwable = new HttpException($config['status_code'], $throwable->getMessage(), $throwable, $headers); + $throwable = HttpException::fromStatusCode($config['status_code'], $throwable->getMessage(), $throwable, $headers); $event->setThrowable($throwable); } break; } // There's no specific status code defined in the configuration for this exception - if (!$throwable instanceof HttpExceptionInterface) { - $class = new \ReflectionClass($throwable); - - do { - if ($attributes = $class->getAttributes(WithHttpStatus::class, \ReflectionAttribute::IS_INSTANCEOF)) { - /** @var WithHttpStatus $instance */ - $instance = $attributes[0]->newInstance(); - - $throwable = new HttpException($instance->statusCode, $throwable->getMessage(), $throwable, $instance->headers); - $event->setThrowable($throwable); - break; - } - } while ($class = $class->getParentClass()); + if (!$throwable instanceof HttpExceptionInterface && $withHttpStatus = $this->getInheritedAttribute($throwable::class, WithHttpStatus::class)) { + $throwable = HttpException::fromStatusCode($withHttpStatus->statusCode, $throwable->getMessage(), $throwable, $withHttpStatus->headers); + $event->setThrowable($throwable); } $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); } - /** - * @return void - */ - public function onKernelException(ExceptionEvent $event) + public function onKernelException(ExceptionEvent $event): void { if (null === $this->controller) { return; } + if (!$this->debug && $event->isKernelTerminating()) { + return; + } + $throwable = $event->getThrowable(); $exceptionHandler = set_exception_handler('var_dump'); @@ -118,7 +100,7 @@ public function onKernelException(ExceptionEvent $event) } catch (\Exception $e) { $f = FlattenException::createFromThrowable($e); - $this->logException($e, sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), basename($e->getFile()), $e->getLine())); + $this->logException($e, \sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), basename($e->getFile()), $e->getLine())); $prev = $e; do { @@ -147,10 +129,7 @@ public function removeCspHeader(ResponseEvent $event): void } } - /** - * @return void - */ - public function onControllerArguments(ControllerArgumentsEvent $event) + public function onControllerArguments(ControllerArgumentsEvent $event): void { $e = $event->getRequest()->attributes->get('exception'); @@ -182,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) { - return; - } + $logChannel = (3 < \func_num_args() ? func_get_arg(3) : null) ?? $this->resolveLogChannel($exception); $logLevel ??= $this->resolveLogLevel($exception); - $this->logger->log($logLevel, $message, ['exception' => $exception]); + if (!$logger = $this->getLogger($logChannel)) { + return; + } + + $logger->log($logLevel, $message, ['exception' => $exception]); } /** @@ -205,16 +188,9 @@ private function resolveLogLevel(\Throwable $throwable): string } } - $class = new \ReflectionClass($throwable); - - do { - if ($attributes = $class->getAttributes(WithLogLevel::class)) { - /** @var WithLogLevel $instance */ - $instance = $attributes[0]->newInstance(); - - return $instance->level; - } - } while ($class = $class->getParentClass()); + if ($withLogLevel = $this->getInheritedAttribute($throwable::class, WithLogLevel::class)) { + return $withLogLevel->level; + } if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() >= 500) { return LogLevel::CRITICAL; @@ -223,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. */ @@ -231,11 +218,57 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re $attributes = [ '_controller' => $this->controller, 'exception' => $exception, - 'logger' => DebugLoggerConfigurator::getDebugLogger($this->logger), + 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($this->resolveLogChannel($exception))), ]; $request = $request->duplicate(null, null, $attributes); $request->setMethod('GET'); return $request; } + + /** + * @template T + * + * @param class-string $attribute + * + * @return T|null + */ + private function getInheritedAttribute(string $class, string $attribute): ?object + { + $class = new \ReflectionClass($class); + $interfaces = []; + $attributeReflector = null; + $parentInterfaces = []; + $ownInterfaces = []; + + do { + if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { + $attributeReflector = $attributes[0]; + $parentInterfaces = class_implements($class->name); + break; + } + + $interfaces[] = class_implements($class->name); + } while ($class = $class->getParentClass()); + + while ($interfaces) { + $ownInterfaces = array_diff_key(array_pop($interfaces), $parentInterfaces); + $parentInterfaces += $ownInterfaces; + + foreach ($ownInterfaces as $interface) { + $class = new \ReflectionClass($interface); + + if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { + $attributeReflector = $attributes[0]; + } + } + } + + return $attributeReflector?->newInstance(); + } + + private function getLogger(?string $logChannel): ?LoggerInterface + { + return $logChannel ? $this->loggers[$logChannel] ?? $this->logger : $this->logger; + } } diff --git a/EventListener/FragmentListener.php b/EventListener/FragmentListener.php index 562244b338..866ae040b5 100644 --- a/EventListener/FragmentListener.php +++ b/EventListener/FragmentListener.php @@ -33,16 +33,13 @@ */ class FragmentListener implements EventSubscriberInterface { - private UriSigner $signer; - private string $fragmentPath; - /** * @param string $fragmentPath The path that triggers this listener */ - public function __construct(UriSigner $signer, string $fragmentPath = '/_fragment') - { - $this->signer = $signer; - $this->fragmentPath = $fragmentPath; + public function __construct( + private UriSigner $signer, + private string $fragmentPath = '/_fragment', + ) { } /** @@ -70,7 +67,7 @@ public function onKernelRequest(RequestEvent $event): void } parse_str($request->query->get('_path', ''), $attributes); - $attributes['_check_controller_is_allowed'] = -1; // @deprecated, switch to true in Symfony 7 + $attributes['_check_controller_is_allowed'] = true; $request->attributes->add($attributes); $request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes)); $request->query->remove('_path'); diff --git a/EventListener/LocaleAwareListener.php b/EventListener/LocaleAwareListener.php index 83fbb4f8b3..1c78c4bdd3 100644 --- a/EventListener/LocaleAwareListener.php +++ b/EventListener/LocaleAwareListener.php @@ -25,16 +25,13 @@ */ class LocaleAwareListener implements EventSubscriberInterface { - private iterable $localeAwareServices; - private RequestStack $requestStack; - /** * @param iterable $localeAwareServices */ - public function __construct(iterable $localeAwareServices, RequestStack $requestStack) - { - $this->localeAwareServices = $localeAwareServices; - $this->requestStack = $requestStack; + public function __construct( + private iterable $localeAwareServices, + private RequestStack $requestStack, + ) { } public function onKernelRequest(RequestEvent $event): void diff --git a/EventListener/LocaleListener.php b/EventListener/LocaleListener.php index 9feaa0b4f8..b905f77d4a 100644 --- a/EventListener/LocaleListener.php +++ b/EventListener/LocaleListener.php @@ -29,19 +29,13 @@ */ class LocaleListener implements EventSubscriberInterface { - private ?RequestContextAwareInterface $router; - private string $defaultLocale; - private RequestStack $requestStack; - private bool $useAcceptLanguageHeader; - private array $enabledLocales; - - public function __construct(RequestStack $requestStack, string $defaultLocale = 'en', ?RequestContextAwareInterface $router = null, bool $useAcceptLanguageHeader = false, array $enabledLocales = []) - { - $this->defaultLocale = $defaultLocale; - $this->requestStack = $requestStack; - $this->router = $router; - $this->useAcceptLanguageHeader = $useAcceptLanguageHeader; - $this->enabledLocales = $enabledLocales; + public function __construct( + private RequestStack $requestStack, + private string $defaultLocale = 'en', + private ?RequestContextAwareInterface $router = null, + private bool $useAcceptLanguageHeader = false, + private array $enabledLocales = [], + ) { } public function setDefaultLocale(KernelEvent $event): void diff --git a/EventListener/ProfilerListener.php b/EventListener/ProfilerListener.php index 1f30582f4a..ecaaceb948 100644 --- a/EventListener/ProfilerListener.php +++ b/EventListener/ProfilerListener.php @@ -32,15 +32,9 @@ */ class ProfilerListener implements EventSubscriberInterface { - private Profiler $profiler; - private ?RequestMatcherInterface $matcher; - private bool $onlyException; - private bool $onlyMainRequests; private ?\Throwable $exception = null; /** @var \SplObjectStorage */ private \SplObjectStorage $profiles; - private RequestStack $requestStack; - private ?string $collectParameter; /** @var \SplObjectStorage */ private \SplObjectStorage $parents; @@ -48,16 +42,16 @@ class ProfilerListener implements EventSubscriberInterface * @param bool $onlyException True if the profiler only collects data when an exception occurs, false otherwise * @param bool $onlyMainRequests True if the profiler only collects data when the request is the main request, false otherwise */ - public function __construct(Profiler $profiler, RequestStack $requestStack, ?RequestMatcherInterface $matcher = null, bool $onlyException = false, bool $onlyMainRequests = false, ?string $collectParameter = null) - { - $this->profiler = $profiler; - $this->matcher = $matcher; - $this->onlyException = $onlyException; - $this->onlyMainRequests = $onlyMainRequests; + public function __construct( + private Profiler $profiler, + private RequestStack $requestStack, + private ?RequestMatcherInterface $matcher = null, + private bool $onlyException = false, + private bool $onlyMainRequests = false, + private ?string $collectParameter = null, + ) { $this->profiles = new \SplObjectStorage(); $this->parents = new \SplObjectStorage(); - $this->requestStack = $requestStack; - $this->collectParameter = $collectParameter; } /** diff --git a/EventListener/ResponseListener.php b/EventListener/ResponseListener.php index 5825e16e44..690bfafc69 100644 --- a/EventListener/ResponseListener.php +++ b/EventListener/ResponseListener.php @@ -24,13 +24,10 @@ */ class ResponseListener implements EventSubscriberInterface { - private string $charset; - private bool $addContentLanguageHeader; - - public function __construct(string $charset, bool $addContentLanguageHeader = false) - { - $this->charset = $charset; - $this->addContentLanguageHeader = $addContentLanguageHeader; + public function __construct( + private string $charset, + private bool $addContentLanguageHeader = false, + ) { } /** diff --git a/EventListener/RouterListener.php b/EventListener/RouterListener.php index f4406ade49..dd6f5bb214 100644 --- a/EventListener/RouterListener.php +++ b/EventListener/RouterListener.php @@ -41,30 +41,26 @@ */ class RouterListener implements EventSubscriberInterface { - private RequestMatcherInterface|UrlMatcherInterface $matcher; private RequestContext $context; - private ?LoggerInterface $logger; - private RequestStack $requestStack; - private ?string $projectDir; - private bool $debug; /** * @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface) * * @throws \InvalidArgumentException */ - public function __construct(UrlMatcherInterface|RequestMatcherInterface $matcher, RequestStack $requestStack, ?RequestContext $context = null, ?LoggerInterface $logger = null, ?string $projectDir = null, bool $debug = true) - { + public function __construct( + private UrlMatcherInterface|RequestMatcherInterface $matcher, + private RequestStack $requestStack, + ?RequestContext $context = null, + private ?LoggerInterface $logger = null, + private ?string $projectDir = null, + private bool $debug = true, + ) { if (null === $context && !$matcher instanceof RequestContextAwareInterface) { throw new \InvalidArgumentException('You must either pass a RequestContext or the matcher must implement RequestContextAwareInterface.'); } - $this->matcher = $matcher; $this->context = $context ?? $matcher->getContext(); - $this->requestStack = $requestStack; - $this->logger = $logger; - $this->projectDir = $projectDir; - $this->debug = $debug; } private function setCurrentRequest(?Request $request): void @@ -114,19 +110,52 @@ public function onKernelRequest(RequestEvent $event): void 'method' => $request->getMethod(), ]); - $request->attributes->add($parameters); + $attributes = $parameters; + if ($mapping = $parameters['_route_mapping'] ?? false) { + unset($parameters['_route_mapping']); + $mappedAttributes = []; + $attributes = []; + + foreach ($parameters as $parameter => $value) { + 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; + $mappedAttributes[$attribute] = $parameter; + } elseif ('' !== $mappedAttributes[$attribute]) { + $attributes[$attribute] = [ + $mappedAttributes[$attribute] => $attributes[$attribute], + $parameter => $value, + ]; + $mappedAttributes[$attribute] = ''; + } else { + $attributes[$attribute][$parameter] = $value; + } + } + + $attributes['_route_mapping'] = $mapping; + } + + $request->attributes->add($attributes); unset($parameters['_route'], $parameters['_controller']); $request->attributes->set('_route_params', $parameters); } catch (ResourceNotFoundException $e) { - $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); + $message = \sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); if ($referer = $request->headers->get('referer')) { - $message .= sprintf(' (from "%s")', $referer); + $message .= \sprintf(' (from "%s")', $referer); } throw new NotFoundHttpException($message, $e); } catch (MethodNotAllowedException $e) { - $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getUriForPath($request->getPathInfo()), implode(', ', $e->getAllowedMethods())); + $message = \sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getUriForPath($request->getPathInfo()), implode(', ', $e->getAllowedMethods())); throw new MethodNotAllowedHttpException($e->getAllowedMethods(), $message, $e); } diff --git a/EventListener/SessionListener.php b/EventListener/SessionListener.php index ec23a2e988..565268518f 100644 --- a/EventListener/SessionListener.php +++ b/EventListener/SessionListener.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\EventListener; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; /** @@ -22,6 +23,14 @@ */ class SessionListener extends AbstractSessionListener { + public function __construct( + private ?ContainerInterface $container = null, + bool $debug = false, + array $sessionOptions = [], + ) { + parent::__construct($container, $debug, $sessionOptions); + } + protected function getSession(): ?SessionInterface { if ($this->container->has('session_factory')) { diff --git a/EventListener/StreamedResponseListener.php b/EventListener/StreamedResponseListener.php deleted file mode 100644 index 312d5ee23b..0000000000 --- a/EventListener/StreamedResponseListener.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\EventListener; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\KernelEvents; - -trigger_deprecation('symfony/http-kernel', '6.1', 'The "%s" class is deprecated.', StreamedResponseListener::class); - -/** - * StreamedResponseListener is responsible for sending the Response - * to the client. - * - * @author Fabien Potencier - * - * @final - * - * @deprecated since Symfony 6.1 - */ -class StreamedResponseListener implements EventSubscriberInterface -{ - /** - * Filters the Response. - */ - public function onKernelResponse(ResponseEvent $event): void - { - if (!$event->isMainRequest()) { - return; - } - - $response = $event->getResponse(); - - if ($response instanceof StreamedResponse) { - $response->send(); - } - } - - public static function getSubscribedEvents(): array - { - return [ - KernelEvents::RESPONSE => ['onKernelResponse', -1024], - ]; - } -} diff --git a/EventListener/SurrogateListener.php b/EventListener/SurrogateListener.php index a702a68f84..f6db59e36e 100644 --- a/EventListener/SurrogateListener.php +++ b/EventListener/SurrogateListener.php @@ -26,11 +26,9 @@ */ class SurrogateListener implements EventSubscriberInterface { - private ?SurrogateInterface $surrogate; - - public function __construct(?SurrogateInterface $surrogate = null) - { - $this->surrogate = $surrogate; + public function __construct( + private ?SurrogateInterface $surrogate = null, + ) { } /** diff --git a/Exception/HttpException.php b/Exception/HttpException.php index 6d2c253a33..9a71bcbd6f 100644 --- a/Exception/HttpException.php +++ b/Exception/HttpException.php @@ -18,15 +18,35 @@ */ class HttpException extends \RuntimeException implements HttpExceptionInterface { - private int $statusCode; - private array $headers; + public function __construct( + private int $statusCode, + string $message = '', + ?\Throwable $previous = null, + private array $headers = [], + int $code = 0, + ) { + parent::__construct($message, $code, $previous); + } - public function __construct(int $statusCode, string $message = '', ?\Throwable $previous = null, array $headers = [], int $code = 0) + public static function fromStatusCode(int $statusCode, string $message = '', ?\Throwable $previous = null, array $headers = [], int $code = 0): self { - $this->statusCode = $statusCode; - $this->headers = $headers; - - parent::__construct($message, $code, $previous); + return match ($statusCode) { + 400 => new BadRequestHttpException($message, $previous, $code, $headers), + 403 => new AccessDeniedHttpException($message, $previous, $code, $headers), + 404 => new NotFoundHttpException($message, $previous, $code, $headers), + 406 => new NotAcceptableHttpException($message, $previous, $code, $headers), + 409 => new ConflictHttpException($message, $previous, $code, $headers), + 410 => new GoneHttpException($message, $previous, $code, $headers), + 411 => new LengthRequiredHttpException($message, $previous, $code, $headers), + 412 => new PreconditionFailedHttpException($message, $previous, $code, $headers), + 423 => new LockedHttpException($message, $previous, $code, $headers), + 415 => new UnsupportedMediaTypeHttpException($message, $previous, $code, $headers), + 422 => new UnprocessableEntityHttpException($message, $previous, $code, $headers), + 428 => new PreconditionRequiredHttpException($message, $previous, $code, $headers), + 429 => new TooManyRequestsHttpException(null, $message, $previous, $code, $headers), + 503 => new ServiceUnavailableHttpException(null, $message, $previous, $code, $headers), + default => new static($statusCode, $message, $previous, $headers, $code), + }; } public function getStatusCode(): int @@ -39,10 +59,7 @@ public function getHeaders(): array return $this->headers; } - /** - * @return void - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } diff --git a/Exception/NearMissValueResolverException.php b/Exception/NearMissValueResolverException.php new file mode 100644 index 0000000000..73ccfe916a --- /dev/null +++ b/Exception/NearMissValueResolverException.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\Exception; + +/** + * Lets value resolvers tell when an argument could be under their watch but failed to be resolved. + * + * Throwing this exception inside `ValueResolverInterface::resolve` does not interrupt the value resolvers chain. + */ +class NearMissValueResolverException extends \RuntimeException +{ +} diff --git a/Exception/ResolverNotFoundException.php b/Exception/ResolverNotFoundException.php index 6d9fb8a01f..aa859d5115 100644 --- a/Exception/ResolverNotFoundException.php +++ b/Exception/ResolverNotFoundException.php @@ -18,7 +18,7 @@ class ResolverNotFoundException extends \RuntimeException */ public function __construct(string $name, array $alternatives = []) { - $msg = sprintf('You have requested a non-existent resolver "%s".', $name); + $msg = \sprintf('You have requested a non-existent resolver "%s".', $name); if ($alternatives) { if (1 === \count($alternatives)) { $msg .= ' Did you mean this: "'; diff --git a/Fragment/AbstractSurrogateFragmentRenderer.php b/Fragment/AbstractSurrogateFragmentRenderer.php index 7eea1aed44..ddd5bfcb07 100644 --- a/Fragment/AbstractSurrogateFragmentRenderer.php +++ b/Fragment/AbstractSurrogateFragmentRenderer.php @@ -24,21 +24,17 @@ */ abstract class AbstractSurrogateFragmentRenderer extends RoutableFragmentRenderer { - private ?SurrogateInterface $surrogate; - private FragmentRendererInterface $inlineStrategy; - private ?UriSigner $signer; - /** * The "fallback" strategy when surrogate is not available should always be an * instance of InlineFragmentRenderer. * * @param FragmentRendererInterface $inlineStrategy The inline strategy to use when the surrogate is not supported */ - public function __construct(?SurrogateInterface $surrogate, FragmentRendererInterface $inlineStrategy, ?UriSigner $signer = null) - { - $this->surrogate = $surrogate; - $this->inlineStrategy = $inlineStrategy; - $this->signer = $signer; + public function __construct( + private ?SurrogateInterface $surrogate, + private FragmentRendererInterface $inlineStrategy, + private ?UriSigner $signer = null, + ) { } /** @@ -59,7 +55,7 @@ public function __construct(?SurrogateInterface $surrogate, FragmentRendererInte public function render(string|ControllerReference $uri, Request $request, array $options = []): Response { if (!$this->surrogate || !$this->surrogate->hasSurrogateCapability($request)) { - $request->attributes->set('_check_controller_is_allowed', -1); // @deprecated, switch to true in Symfony 7 + $request->attributes->set('_check_controller_is_allowed', true); if ($uri instanceof ControllerReference && $this->containsNonScalars($uri->attributes)) { throw new \InvalidArgumentException('Passing non-scalar values as part of URI attributes to the ESI and SSI rendering strategies is not supported. Use a different rendering strategy or pass scalar values.'); diff --git a/Fragment/FragmentHandler.php b/Fragment/FragmentHandler.php index 62b21e6d4e..cfd5c27d8e 100644 --- a/Fragment/FragmentHandler.php +++ b/Fragment/FragmentHandler.php @@ -29,29 +29,27 @@ */ class FragmentHandler { - private bool $debug; + /** @var array */ private array $renderers = []; - private RequestStack $requestStack; /** * @param FragmentRendererInterface[] $renderers An array of FragmentRendererInterface instances * @param bool $debug Whether the debug mode is enabled or not */ - public function __construct(RequestStack $requestStack, array $renderers = [], bool $debug = false) - { - $this->requestStack = $requestStack; + public function __construct( + private RequestStack $requestStack, + array $renderers = [], + private bool $debug = false, + ) { foreach ($renderers as $renderer) { $this->addRenderer($renderer); } - $this->debug = $debug; } /** * Adds a renderer. - * - * @return void */ - public function addRenderer(FragmentRendererInterface $renderer) + public function addRenderer(FragmentRendererInterface $renderer): void { $this->renderers[$renderer->getName()] = $renderer; } @@ -73,7 +71,7 @@ public function render(string|ControllerReference $uri, string $renderer = 'inli } if (!isset($this->renderers[$renderer])) { - throw new \InvalidArgumentException(sprintf('The "%s" renderer does not exist.', $renderer)); + throw new \InvalidArgumentException(\sprintf('The "%s" renderer does not exist.', $renderer)); } if (!$request = $this->requestStack->getCurrentRequest()) { @@ -97,7 +95,7 @@ protected function deliver(Response $response): ?string { if (!$response->isSuccessful()) { $responseStatusCode = $response->getStatusCode(); - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %d).', $this->requestStack->getCurrentRequest()->getUri(), $responseStatusCode), 0, new HttpException($responseStatusCode)); + throw new \RuntimeException(\sprintf('Error when rendering "%s" (Status code is %d).', $this->requestStack->getCurrentRequest()->getUri(), $responseStatusCode), 0, new HttpException($responseStatusCode)); } if (!$response instanceof StreamedResponse) { diff --git a/Fragment/FragmentUriGenerator.php b/Fragment/FragmentUriGenerator.php index 59423293e8..832b0483d1 100644 --- a/Fragment/FragmentUriGenerator.php +++ b/Fragment/FragmentUriGenerator.php @@ -24,15 +24,11 @@ */ final class FragmentUriGenerator implements FragmentUriGeneratorInterface { - private string $fragmentPath; - private ?UriSigner $signer; - private ?RequestStack $requestStack; - - public function __construct(string $fragmentPath, ?UriSigner $signer = null, ?RequestStack $requestStack = null) - { - $this->fragmentPath = $fragmentPath; - $this->signer = $signer; - $this->requestStack = $requestStack; + public function __construct( + private string $fragmentPath, + private ?UriSigner $signer = null, + private ?RequestStack $requestStack = null, + ) { } public function generate(ControllerReference $controller, ?Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string @@ -83,7 +79,7 @@ private function checkNonScalar(array $values): void if (\is_array($value)) { $this->checkNonScalar($value); } elseif (!\is_scalar($value) && null !== $value) { - throw new \LogicException(sprintf('Controller attributes cannot contain non-scalar/non-null values (value for key "%s" is not a scalar or null).', $key)); + throw new \LogicException(\sprintf('Controller attributes cannot contain non-scalar/non-null values (value for key "%s" is not a scalar or null).', $key)); } } } diff --git a/Fragment/HIncludeFragmentRenderer.php b/Fragment/HIncludeFragmentRenderer.php index edcf9938c4..4cc77c419e 100644 --- a/Fragment/HIncludeFragmentRenderer.php +++ b/Fragment/HIncludeFragmentRenderer.php @@ -24,20 +24,15 @@ */ class HIncludeFragmentRenderer extends RoutableFragmentRenderer { - private ?string $globalDefaultTemplate; - private ?UriSigner $signer; - private ?Environment $twig; - private string $charset; - /** * @param string|null $globalDefaultTemplate The global default content (it can be a template name or the content) */ - public function __construct(?Environment $twig = null, ?UriSigner $signer = null, ?string $globalDefaultTemplate = null, string $charset = 'utf-8') - { - $this->twig = $twig; - $this->globalDefaultTemplate = $globalDefaultTemplate; - $this->signer = $signer; - $this->charset = $charset; + public function __construct( + private ?Environment $twig = null, + private ?UriSigner $signer = null, + private ?string $globalDefaultTemplate = null, + private string $charset = 'utf-8', + ) { } /** @@ -79,7 +74,7 @@ public function render(string|ControllerReference $uri, Request $request, array if (\count($attributes) > 0) { $flags = \ENT_QUOTES | \ENT_SUBSTITUTE; foreach ($attributes as $attribute => $value) { - $renderedAttributes .= sprintf( + $renderedAttributes .= \sprintf( ' %s="%s"', htmlspecialchars($attribute, $flags, $this->charset, false), htmlspecialchars($value, $flags, $this->charset, false) @@ -87,7 +82,7 @@ public function render(string|ControllerReference $uri, Request $request, array } } - return new Response(sprintf('%s', $uri, $renderedAttributes, $content)); + return new Response(\sprintf('%s', $uri, $renderedAttributes, $content)); } public function getName(): string diff --git a/Fragment/InlineFragmentRenderer.php b/Fragment/InlineFragmentRenderer.php index 1999603a3b..2dbe7be602 100644 --- a/Fragment/InlineFragmentRenderer.php +++ b/Fragment/InlineFragmentRenderer.php @@ -27,13 +27,10 @@ */ class InlineFragmentRenderer extends RoutableFragmentRenderer { - private HttpKernelInterface $kernel; - private ?EventDispatcherInterface $dispatcher; - - public function __construct(HttpKernelInterface $kernel, ?EventDispatcherInterface $dispatcher = null) - { - $this->kernel = $kernel; - $this->dispatcher = $dispatcher; + public function __construct( + private HttpKernelInterface $kernel, + private ?EventDispatcherInterface $dispatcher = null, + ) { } /** @@ -103,10 +100,7 @@ public function render(string|ControllerReference $uri, Request $request, array } } - /** - * @return Request - */ - protected function createSubRequest(string $uri, Request $request) + protected function createSubRequest(string $uri, Request $request): Request { $cookies = $request->cookies->all(); $server = $request->server->all(); diff --git a/Fragment/RoutableFragmentRenderer.php b/Fragment/RoutableFragmentRenderer.php index 6a8989081f..f9b400c902 100644 --- a/Fragment/RoutableFragmentRenderer.php +++ b/Fragment/RoutableFragmentRenderer.php @@ -31,10 +31,8 @@ abstract class RoutableFragmentRenderer implements FragmentRendererInterface * Sets the fragment path that triggers the fragment listener. * * @see FragmentListener - * - * @return void */ - public function setFragmentPath(string $path) + public function setFragmentPath(string $path): void { $this->fragmentPath = $path; } diff --git a/HttpCache/AbstractSurrogate.php b/HttpCache/AbstractSurrogate.php index 95518bed2b..07a125af53 100644 --- a/HttpCache/AbstractSurrogate.php +++ b/HttpCache/AbstractSurrogate.php @@ -23,23 +23,13 @@ */ abstract class AbstractSurrogate implements SurrogateInterface { - protected $contentTypes; - - /** - * @deprecated since Symfony 6.3 - */ - protected $phpEscapeMap = [ - ['', '', '', ''], - ]; - /** * @param array $contentTypes An array of content-type that should be parsed for Surrogate information * (default: text/html, text/xml, application/xhtml+xml, and application/xml) */ - public function __construct(array $contentTypes = ['text/html', 'text/xml', 'application/xhtml+xml', 'application/xml']) - { - $this->contentTypes = $contentTypes; + public function __construct( + protected array $contentTypes = ['text/html', 'text/xml', 'application/xhtml+xml', 'application/xml'], + ) { } /** @@ -56,16 +46,13 @@ public function hasSurrogateCapability(Request $request): bool return false; } - return str_contains($value, sprintf('%s/1.0', strtoupper($this->getName()))); + return str_contains($value, \sprintf('%s/1.0', strtoupper($this->getName()))); } - /** - * @return void - */ - public function addSurrogateCapability(Request $request) + public function addSurrogateCapability(Request $request): void { $current = $request->headers->get('Surrogate-Capability'); - $new = sprintf('symfony="%s/1.0"', strtoupper($this->getName())); + $new = \sprintf('symfony="%s/1.0"', strtoupper($this->getName())); $request->headers->set('Surrogate-Capability', $current ? $current.', '.$new : $new); } @@ -76,7 +63,7 @@ public function needsParsing(Response $response): bool return false; } - $pattern = sprintf('#content="[^"]*%s/1.0[^"]*"#', strtoupper($this->getName())); + $pattern = \sprintf('#content="[^"]*%s/1.0[^"]*"#', strtoupper($this->getName())); return (bool) preg_match($pattern, $control); } @@ -89,7 +76,7 @@ public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreE $response = $cache->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); if (!$response->isSuccessful() && Response::HTTP_NOT_MODIFIED !== $response->getStatusCode()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %d).', $subRequest->getUri(), $response->getStatusCode())); + throw new \RuntimeException(\sprintf('Error when rendering "%s" (Status code is %d).', $subRequest->getUri(), $response->getStatusCode())); } return $response->getContent(); @@ -108,10 +95,8 @@ public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreE /** * Remove the Surrogate from the Surrogate-Control header. - * - * @return void */ - protected function removeFromControl(Response $response) + protected function removeFromControl(Response $response): void { if (!$response->headers->has('Surrogate-Control')) { return; @@ -120,12 +105,12 @@ protected function removeFromControl(Response $response) $value = $response->headers->get('Surrogate-Control'); $upperName = strtoupper($this->getName()); - if (sprintf('content="%s/1.0"', $upperName) == $value) { + if (\sprintf('content="%s/1.0"', $upperName) == $value) { $response->headers->remove('Surrogate-Control'); - } elseif (preg_match(sprintf('#,\s*content="%s/1.0"#', $upperName), $value)) { - $response->headers->set('Surrogate-Control', preg_replace(sprintf('#,\s*content="%s/1.0"#', $upperName), '', $value)); - } elseif (preg_match(sprintf('#content="%s/1.0",\s*#', $upperName), $value)) { - $response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value)); + } elseif (preg_match(\sprintf('#,\s*content="%s/1.0"#', $upperName), $value)) { + $response->headers->set('Surrogate-Control', preg_replace(\sprintf('#,\s*content="%s/1.0"#', $upperName), '', $value)); + } elseif (preg_match(\sprintf('#content="%s/1.0",\s*#', $upperName), $value)) { + $response->headers->set('Surrogate-Control', preg_replace(\sprintf('#content="%s/1.0",\s*#', $upperName), '', $value)); } } diff --git a/Tests/Fixtures/KernelForTestWithLoadClassCache.php b/HttpCache/CacheWasLockedException.php similarity index 58% rename from Tests/Fixtures/KernelForTestWithLoadClassCache.php rename to HttpCache/CacheWasLockedException.php index 080953fe02..f13946ad71 100644 --- a/Tests/Fixtures/KernelForTestWithLoadClassCache.php +++ b/HttpCache/CacheWasLockedException.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Tests\Fixtures; +namespace Symfony\Component\HttpKernel\HttpCache; -class KernelForTestWithLoadClassCache extends KernelForTest +/** + * @internal + */ +class CacheWasLockedException extends \Exception { - public function doLoadClassCache(): void - { - } } diff --git a/HttpCache/Esi.php b/HttpCache/Esi.php index e8faf0fdbd..5866c9e593 100644 --- a/HttpCache/Esi.php +++ b/HttpCache/Esi.php @@ -32,10 +32,7 @@ public function getName(): string return 'esi'; } - /** - * @return void - */ - public function addSurrogateControl(Response $response) + public function addSurrogateControl(Response $response): void { if (str_contains($response->getContent(), 'headers->set('Surrogate-Control', 'content="ESI/1.0"'); @@ -44,14 +41,14 @@ public function addSurrogateControl(Response $response) public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreErrors = true, string $comment = ''): string { - $html = sprintf('', + $html = \sprintf('', $uri, $ignoreErrors ? ' onerror="continue"' : '', - $alt ? sprintf(' alt="%s"', $alt) : '' + $alt ? \sprintf(' alt="%s"', $alt) : '' ); - if (!empty($comment)) { - return sprintf("\n%s", $comment, $html); + if ($comment) { + return \sprintf("\n%s", $comment, $html); } return $html; @@ -60,12 +57,12 @@ public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreE public function process(Request $request, Response $response): Response { $type = $response->headers->get('Content-Type'); - if (empty($type)) { + if (!$type) { $type = 'text/html'; } $parts = explode(';', $type); - if (!\in_array($parts[0], $this->contentTypes)) { + if (!\in_array($parts[0], $this->contentTypes, true)) { return $response; } diff --git a/HttpCache/HttpCache.php b/HttpCache/HttpCache.php index 3b484e5c3e..2b1be6a95a 100644 --- a/HttpCache/HttpCache.php +++ b/HttpCache/HttpCache.php @@ -32,10 +32,7 @@ class HttpCache implements HttpKernelInterface, TerminableInterface { public const BODY_EVAL_BOUNDARY_LENGTH = 24; - private HttpKernelInterface $kernel; - private StoreInterface $store; private Request $request; - private ?SurrogateInterface $surrogate; private ?ResponseCacheStrategyInterface $surrogateCacheStrategy = null; private array $options = []; private array $traces = []; @@ -84,18 +81,13 @@ class HttpCache implements HttpKernelInterface, TerminableInterface * the cache can serve a stale response when an error is encountered (default: 60). * This setting is overridden by the stale-if-error HTTP Cache-Control extension * (see RFC 5861). - * - * * terminate_on_cache_hit Specifies if the kernel.terminate event should be dispatched even when the cache - * was hit (default: true). - * Unless your application needs to process events on cache hits, it is recommended - * to set this to false to avoid having to bootstrap the Symfony framework on a cache hit. */ - public function __construct(HttpKernelInterface $kernel, StoreInterface $store, ?SurrogateInterface $surrogate = null, array $options = []) - { - $this->store = $store; - $this->kernel = $kernel; - $this->surrogate = $surrogate; - + public function __construct( + private HttpKernelInterface $kernel, + private StoreInterface $store, + private ?SurrogateInterface $surrogate = null, + array $options = [], + ) { // needed in case there is a fatal error because the backend is too slow to respond register_shutdown_function($this->store->cleanup(...)); @@ -110,7 +102,6 @@ public function __construct(HttpKernelInterface $kernel, StoreInterface $store, 'stale_if_error' => 60, 'trace_level' => 'none', 'trace_header' => 'X-Symfony-Cache', - 'terminate_on_cache_hit' => true, ], $options); if (!isset($options['trace_level'])) { @@ -158,7 +149,7 @@ public function getLog(): string { $log = []; foreach ($this->traces as $request => $traces) { - $log[] = sprintf('%s: %s', $request, implode(', ', $traces)); + $log[] = \sprintf('%s: %s', $request, implode(', ', $traces)); } return implode('; ', $log); @@ -219,7 +210,13 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R $this->record($request, 'reload'); $response = $this->fetch($request, $catch); } else { - $response = $this->lookup($request, $catch); + $response = null; + do { + try { + $response = $this->lookup($request, $catch); + } catch (CacheWasLockedException) { + } + } while (null === $response); } $this->restoreResponseBody($request, $response); @@ -245,17 +242,12 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R return $response; } - /** - * @return void - */ - public function terminate(Request $request, Response $response) + public function terminate(Request $request, Response $response): void { // Do not call any listeners in case of a cache hit. // This ensures identical behavior as if you had a separate // reverse caching proxy such as Varnish and the like. - if ($this->options['terminate_on_cache_hit']) { - trigger_deprecation('symfony/http-kernel', '6.2', 'Setting "terminate_on_cache_hit" to "true" is deprecated and will be changed to "false" in Symfony 7.0.'); - } elseif (\in_array('fresh', $this->traces[$this->getTraceKey($request)] ?? [], true)) { + if (\in_array('fresh', $this->traces[$this->getTraceKey($request)] ?? [], true)) { return; } @@ -404,7 +396,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal // return the response and not the cache entry if the response is valid but not cached $etag = $response->getEtag(); - if ($etag && \in_array($etag, $requestEtags) && !\in_array($etag, $cachedEtags)) { + if ($etag && \in_array($etag, $requestEtags, true) && !\in_array($etag, $cachedEtags, true)) { return $response; } @@ -465,10 +457,8 @@ protected function fetch(Request $request, bool $catch = false): Response * * @param bool $catch Whether to catch exceptions or not * @param Response|null $entry A Response instance (the stale entry if present, null otherwise) - * - * @return Response */ - protected function forward(Request $request, bool $catch = false, ?Response $entry = null) + protected function forward(Request $request, bool $catch = false, ?Response $entry = null): Response { $this->surrogate?->addSurrogateCapability($request); @@ -576,15 +566,7 @@ protected function lock(Request $request, Response $entry): bool // wait for the lock to be released if ($this->waitForLock($request)) { - // replace the current entry with the fresh one - $new = $this->lookup($request); - $entry->headers = $new->headers; - $entry->setContent($new->getContent()); - $entry->setStatusCode($new->getStatusCode()); - $entry->setProtocolVersion($new->getProtocolVersion()); - foreach ($new->headers->getCookies() as $cookie) { - $entry->headers->setCookie($cookie); - } + throw new CacheWasLockedException(); // unwind back to handle(), try again } else { // backend is slow as hell, send a 503 response (to avoid the dog pile effect) $entry->setStatusCode(503); @@ -598,11 +580,9 @@ protected function lock(Request $request, Response $entry): bool /** * Writes the Response to the cache. * - * @return void - * * @throws \Exception */ - protected function store(Request $request, Response $response) + protected function store(Request $request, Response $response): void { try { $restoreHeaders = []; @@ -677,10 +657,7 @@ private function restoreResponseBody(Request $request, Response $response): void $response->headers->remove('X-Body-File'); } - /** - * @return void - */ - protected function processResponseBody(Request $request, Response $response) + protected function processResponseBody(Request $request, Response $response): void { if ($this->surrogate?->needsParsing($response)) { $this->surrogate->process($request, $response); @@ -728,7 +705,7 @@ private function getTraceKey(Request $request): string try { return $request->getMethod().' '.$path; - } catch (SuspiciousOperationException $e) { + } catch (SuspiciousOperationException) { return '_BAD_METHOD_ '.$path; } } diff --git a/HttpCache/ResponseCacheStrategy.php b/HttpCache/ResponseCacheStrategy.php index bf7ec78f20..4aba46728d 100644 --- a/HttpCache/ResponseCacheStrategy.php +++ b/HttpCache/ResponseCacheStrategy.php @@ -37,7 +37,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface private int $embeddedResponses = 0; private bool $isNotCacheableResponseEmbedded = false; private int $age = 0; - private \DateTimeInterface|null|false $lastModified = null; + private \DateTimeInterface|false|null $lastModified = null; private array $flagDirectives = [ 'no-cache' => null, 'no-store' => null, @@ -54,10 +54,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface 'expires' => false, ]; - /** - * @return void - */ - public function add(Response $response) + public function add(Response $response): void { ++$this->embeddedResponses; @@ -113,10 +110,7 @@ public function add(Response $response) } } - /** - * @return void - */ - public function update(Response $response) + public function update(Response $response): void { // if we have no embedded Response, do nothing if (0 === $this->embeddedResponses) { @@ -228,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; } diff --git a/HttpCache/ResponseCacheStrategyInterface.php b/HttpCache/ResponseCacheStrategyInterface.php index 33c8bd9412..6143a13c83 100644 --- a/HttpCache/ResponseCacheStrategyInterface.php +++ b/HttpCache/ResponseCacheStrategyInterface.php @@ -27,15 +27,11 @@ interface ResponseCacheStrategyInterface { /** * Adds a Response. - * - * @return void */ - public function add(Response $response); + public function add(Response $response): void; /** * Updates the Response HTTP headers based on the embedded Responses. - * - * @return void */ - public function update(Response $response); + public function update(Response $response): void; } diff --git a/HttpCache/Ssi.php b/HttpCache/Ssi.php index 8cf4e49077..4349e8f57e 100644 --- a/HttpCache/Ssi.php +++ b/HttpCache/Ssi.php @@ -26,10 +26,7 @@ public function getName(): string return 'ssi'; } - /** - * @return void - */ - public function addSurrogateControl(Response $response) + public function addSurrogateControl(Response $response): void { if (str_contains($response->getContent(), '', $uri); + return \sprintf('', $uri); } public function process(Request $request, Response $response): Response { $type = $response->headers->get('Content-Type'); - if (empty($type)) { + if (!$type) { $type = 'text/html'; } $parts = explode(';', $type); - if (!\in_array($parts[0], $this->contentTypes)) { + if (!\in_array($parts[0], $this->contentTypes, true)) { return $response; } diff --git a/HttpCache/Store.php b/HttpCache/Store.php index 7f7f1a1a14..9c790f6d82 100644 --- a/HttpCache/Store.php +++ b/HttpCache/Store.php @@ -24,12 +24,10 @@ */ class Store implements StoreInterface { - protected $root; /** @var \SplObjectStorage */ private \SplObjectStorage $keyCache; /** @var array */ private array $locks = []; - private array $options; /** * Constructor. @@ -41,24 +39,21 @@ class Store implements StoreInterface * * @throws \RuntimeException */ - public function __construct(string $root, array $options = []) - { - $this->root = $root; + public function __construct( + protected string $root, + private array $options = [], + ) { if (!is_dir($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) { - throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root)); + throw new \RuntimeException(\sprintf('Unable to create the store directory (%s).', $this->root)); } $this->keyCache = new \SplObjectStorage(); - $this->options = array_merge([ - 'private_headers' => ['Set-Cookie'], - ], $options); + $this->options['private_headers'] ??= ['Set-Cookie']; } /** * Cleanups storage. - * - * @return void */ - public function cleanup() + public function cleanup(): void { // unlock everything foreach ($this->locks as $lock) { @@ -249,11 +244,9 @@ protected function generateContentDigest(Response $response): string /** * Invalidates all cache entries that match the request. * - * @return void - * * @throws \RuntimeException */ - public function invalidate(Request $request) + public function invalidate(Request $request): void { $modified = false; $key = $this->getCacheKey($request); @@ -285,7 +278,7 @@ public function invalidate(Request $request) */ private function requestsMatch(?string $vary, array $env1, array $env2): bool { - if (empty($vary)) { + if (!$vary) { return true; } @@ -417,10 +410,7 @@ private function save(string $key, string $data, bool $overwrite = true): bool return true; } - /** - * @return string - */ - public function getPath(string $key) + public function getPath(string $key): string { return $this->root.\DIRECTORY_SEPARATOR.substr($key, 0, 2).\DIRECTORY_SEPARATOR.substr($key, 2, 2).\DIRECTORY_SEPARATOR.substr($key, 4, 2).\DIRECTORY_SEPARATOR.substr($key, 6); } diff --git a/HttpCache/StoreInterface.php b/HttpCache/StoreInterface.php index b73cb7a9e6..1fd3f05eca 100644 --- a/HttpCache/StoreInterface.php +++ b/HttpCache/StoreInterface.php @@ -41,10 +41,8 @@ public function write(Request $request, Response $response): string; /** * Invalidates all cache entries that match the request. - * - * @return void */ - public function invalidate(Request $request); + public function invalidate(Request $request): void; /** * Locks the cache for a given Request. @@ -76,8 +74,6 @@ public function purge(string $url): bool; /** * Cleanups storage. - * - * @return void */ - public function cleanup(); + public function cleanup(): void; } diff --git a/HttpCache/SubRequestHandler.php b/HttpCache/SubRequestHandler.php index 253071f07d..4caf3daf2e 100644 --- a/HttpCache/SubRequestHandler.php +++ b/HttpCache/SubRequestHandler.php @@ -51,16 +51,16 @@ public static function handle(HttpKernelInterface $kernel, Request $request, int $trustedValues = []; foreach (array_reverse($request->getClientIps()) as $ip) { $trustedIps[] = $ip; - $trustedValues[] = sprintf('for="%s"', $ip); + $trustedValues[] = \sprintf('for="%s"', $ip); } if ($ip !== $remoteAddr) { $trustedIps[] = $remoteAddr; - $trustedValues[] = sprintf('for="%s"', $remoteAddr); + $trustedValues[] = \sprintf('for="%s"', $remoteAddr); } // set trusted values, reusing as much as possible the global trusted settings if (Request::HEADER_FORWARDED & $trustedHeaderSet) { - $trustedValues[0] .= sprintf(';host="%s";proto=%s', $request->getHttpHost(), $request->getScheme()); + $trustedValues[0] .= \sprintf(';host="%s";proto=%s', $request->getHttpHost(), $request->getScheme()); $request->headers->set('Forwarded', $v = implode(', ', $trustedValues)); $request->server->set('HTTP_FORWARDED', $v); } diff --git a/HttpCache/SurrogateInterface.php b/HttpCache/SurrogateInterface.php index 5ff10c963e..45b358b7b3 100644 --- a/HttpCache/SurrogateInterface.php +++ b/HttpCache/SurrogateInterface.php @@ -33,19 +33,15 @@ public function hasSurrogateCapability(Request $request): bool; /** * Adds Surrogate-capability to the given Request. - * - * @return void */ - public function addSurrogateCapability(Request $request); + public function addSurrogateCapability(Request $request): void; /** * Adds HTTP headers to specify that the Response needs to be parsed for Surrogate. * * This method only adds an Surrogate HTTP header if the Response has some Surrogate tags. - * - * @return void */ - public function addSurrogateControl(Response $response); + public function addSurrogateControl(Response $response): void; /** * Checks that the Response needs to be parsed for Surrogate tags. diff --git a/HttpClientKernel.php b/HttpClientKernel.php index 7c719e8e61..ebda2750da 100644 --- a/HttpClientKernel.php +++ b/HttpClientKernel.php @@ -36,7 +36,7 @@ final class HttpClientKernel implements HttpKernelInterface public function __construct(?HttpClientInterface $client = null) { if (null === $client && !class_exists(HttpClient::class)) { - throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); + throw new \LogicException(\sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); } $this->client = $client ?? HttpClient::create(); diff --git a/HttpKernel.php b/HttpKernel.php index 6460bebbdd..91764366cd 100644 --- a/HttpKernel.php +++ b/HttpKernel.php @@ -51,19 +51,19 @@ class_exists(KernelEvents::class); */ class HttpKernel implements HttpKernelInterface, TerminableInterface { - protected $dispatcher; - protected $resolver; - protected $requestStack; + protected RequestStack $requestStack; private ArgumentResolverInterface $argumentResolver; - private bool $handleAllThrowables; - - public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, ?RequestStack $requestStack = null, ?ArgumentResolverInterface $argumentResolver = null, bool $handleAllThrowables = false) - { - $this->dispatcher = $dispatcher; - $this->resolver = $resolver; + private bool $terminating = false; + + public function __construct( + protected EventDispatcherInterface $dispatcher, + protected ControllerResolverInterface $resolver, + ?RequestStack $requestStack = null, + ?ArgumentResolverInterface $argumentResolver = null, + private bool $handleAllThrowables = false, + ) { $this->requestStack = $requestStack ?? new RequestStack(); $this->argumentResolver = $argumentResolver ?? new ArgumentResolver(); - $this->handleAllThrowables = $handleAllThrowables; } public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response @@ -107,12 +107,14 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R } } - /** - * @return void - */ - public function terminate(Request $request, Response $response) + public function terminate(Request $request, Response $response): void { - $this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE); + try { + $this->terminating = true; + $this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE); + } finally { + $this->terminating = false; + } } /** @@ -162,7 +164,7 @@ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Re // load controller if (false === $controller = $this->resolver->getController($request)) { - throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo())); + throw new NotFoundHttpException(\sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo())); } $event = new ControllerEvent($this, $controller, $request, $type); @@ -188,7 +190,7 @@ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Re if ($event->hasResponse()) { $response = $event->getResponse(); } else { - $msg = sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this->varToString($response)); + $msg = \sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this->varToString($response)); // the user may have forgotten to return something if (null === $response) { @@ -235,7 +237,7 @@ private function finishRequest(Request $request, int $type): void */ private function handleThrowable(\Throwable $e, Request $request, int $type): Response { - $event = new ExceptionEvent($this, $request, $type, $e); + $event = new ExceptionEvent($this, $request, $type, $e, isKernelTerminating: $this->terminating); $this->dispatcher->dispatch($event, KernelEvents::EXCEPTION); // a listener might have replaced the exception @@ -278,20 +280,20 @@ private function handleThrowable(\Throwable $e, Request $request, int $type): Re private function varToString(mixed $var): string { if (\is_object($var)) { - return sprintf('an object of type %s', $var::class); + return \sprintf('an object of type %s', $var::class); } if (\is_array($var)) { $a = []; foreach ($var as $k => $v) { - $a[] = sprintf('%s => ...', $k); + $a[] = \sprintf('%s => ...', $k); } - return sprintf('an array ([%s])', mb_substr(implode(', ', $a), 0, 255)); + return \sprintf('an array ([%s])', mb_substr(implode(', ', $a), 0, 255)); } if (\is_resource($var)) { - return sprintf('a resource (%s)', get_resource_type($var)); + return \sprintf('a resource (%s)', get_resource_type($var)); } if (null === $var) { @@ -307,11 +309,11 @@ private function varToString(mixed $var): string } if (\is_string($var)) { - return sprintf('a string ("%s%s")', mb_substr($var, 0, 255), mb_strlen($var) > 255 ? '...' : ''); + return \sprintf('a string ("%s%s")', mb_substr($var, 0, 255), mb_strlen($var) > 255 ? '...' : ''); } if (is_numeric($var)) { - return sprintf('a number (%s)', (string) $var); + return \sprintf('a number (%s)', (string) $var); } return (string) $var; diff --git a/HttpKernelBrowser.php b/HttpKernelBrowser.php index 169789dda4..db6d0e08a9 100644 --- a/HttpKernelBrowser.php +++ b/HttpKernelBrowser.php @@ -25,21 +25,22 @@ * * @author Fabien Potencier * - * @method Request getRequest() - * @method Response getResponse() + * @template-extends AbstractBrowser */ class HttpKernelBrowser extends AbstractBrowser { - protected $kernel; private bool $catchExceptions = true; /** * @param array $server The server parameters (equivalent of $_SERVER) */ - public function __construct(HttpKernelInterface $kernel, array $server = [], ?History $history = null, ?CookieJar $cookieJar = null) - { + public function __construct( + protected HttpKernelInterface $kernel, + array $server = [], + ?History $history = null, + ?CookieJar $cookieJar = null, + ) { // These class properties must be set before calling the parent constructor, as it may depend on it. - $this->kernel = $kernel; $this->followRedirects = false; parent::__construct($server, $history, $cookieJar); @@ -47,20 +48,16 @@ public function __construct(HttpKernelInterface $kernel, array $server = [], ?Hi /** * Sets whether to catch exceptions when the kernel is handling a request. - * - * @return void */ - public function catchExceptions(bool $catchExceptions) + public function catchExceptions(bool $catchExceptions): void { $this->catchExceptions = $catchExceptions; } /** * @param Request $request - * - * @return Response */ - protected function doRequest(object $request) + protected function doRequest(object $request): Response { $response = $this->kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, $this->catchExceptions); @@ -73,10 +70,8 @@ protected function doRequest(object $request) /** * @param Request $request - * - * @return string */ - protected function getScript(object $request) + protected function getScript(object $request): string { $kernel = var_export(serialize($this->kernel), true); $request = var_export(serialize($request), true); @@ -112,10 +107,7 @@ protected function getScript(object $request) return $code.$this->getHandleScript(); } - /** - * @return string - */ - protected function getHandleScript() + protected function getHandleScript(): string { return <<<'EOF' $response = $kernel->handle($request); diff --git a/HttpKernelInterface.php b/HttpKernelInterface.php index f6c017a4c5..e9415677f7 100644 --- a/HttpKernelInterface.php +++ b/HttpKernelInterface.php @@ -24,12 +24,6 @@ interface HttpKernelInterface public const MAIN_REQUEST = 1; public const SUB_REQUEST = 2; - /** - * @deprecated since symfony/http-kernel 5.3, use MAIN_REQUEST instead. - * To ease the migration, this constant won't be removed until Symfony 7.0. - */ - public const MASTER_REQUEST = self::MAIN_REQUEST; - /** * Handles a Request to convert it to a Response. * diff --git a/Kernel.php b/Kernel.php index bd75891730..42123cea19 100644 --- a/Kernel.php +++ b/Kernel.php @@ -37,7 +37,6 @@ use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\Config\FileLocator; -use Symfony\Component\HttpKernel\DependencyInjection\AddAnnotatedClassesToCachePass; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; // Help opcache.preload discover always-needed symbols @@ -58,13 +57,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl /** * @var array */ - protected $bundles = []; + protected array $bundles = []; - protected $container; - protected $environment; - protected $debug; - protected $booted = false; - protected $startTime; + protected ?ContainerInterface $container = null; + protected bool $booted = false; + protected ?float $startTime = null; private string $projectDir; private ?string $warmupDir = null; @@ -76,23 +73,23 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.4.22'; - public const VERSION_ID = 60422; - public const MAJOR_VERSION = 6; - public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 22; - public const EXTRA_VERSION = ''; + public const VERSION = '7.3.3-DEV'; + public const VERSION_ID = 70303; + public const MAJOR_VERSION = 7; + public const MINOR_VERSION = 3; + public const RELEASE_VERSION = 3; + public const EXTRA_VERSION = 'DEV'; - public const END_OF_MAINTENANCE = '11/2026'; - public const END_OF_LIFE = '11/2027'; + public const END_OF_MAINTENANCE = '01/2026'; + public const END_OF_LIFE = '01/2026'; - public function __construct(string $environment, bool $debug) - { - if (!$this->environment = $environment) { - throw new \InvalidArgumentException(sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this))); + public function __construct( + protected string $environment, + protected bool $debug, + ) { + if (!$environment) { + throw new \InvalidArgumentException(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this))); } - - $this->debug = $debug; } public function __clone() @@ -103,10 +100,7 @@ public function __clone() $this->resetServices = false; } - /** - * @return void - */ - public function boot() + public function boot(): void { if (true === $this->booted) { if (!$this->requestStackSize && $this->resetServices) { @@ -134,20 +128,14 @@ public function boot() $this->booted = true; } - /** - * @return void - */ - public function reboot(?string $warmupDir) + public function reboot(?string $warmupDir): void { $this->shutdown(); $this->warmupDir = $warmupDir; $this->boot(); } - /** - * @return void - */ - public function terminate(Request $request, Response $response) + public function terminate(Request $request, Response $response): void { if (false === $this->booted) { return; @@ -158,10 +146,7 @@ public function terminate(Request $request, Response $response) } } - /** - * @return void - */ - public function shutdown() + public function shutdown(): void { if (false === $this->booted) { return; @@ -216,7 +201,7 @@ public function getBundles(): array public function getBundle(string $name): BundleInterface { if (!isset($this->bundles[$name])) { - throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, get_debug_type($this))); + throw new \InvalidArgumentException(\sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, get_debug_type($this))); } return $this->bundles[$name]; @@ -225,11 +210,11 @@ public function getBundle(string $name): BundleInterface public function locateResource(string $name): string { if ('@' !== $name[0]) { - throw new \InvalidArgumentException(sprintf('A resource name must start with @ ("%s" given).', $name)); + throw new \InvalidArgumentException(\sprintf('A resource name must start with @ ("%s" given).', $name)); } if (str_contains($name, '..')) { - throw new \RuntimeException(sprintf('File name "%s" contains invalid characters (..).', $name)); + throw new \RuntimeException(\sprintf('File name "%s" contains invalid characters (..).', $name)); } $bundleName = substr($name, 1); @@ -243,7 +228,7 @@ public function locateResource(string $name): string return $file; } - throw new \InvalidArgumentException(sprintf('Unable to find file "%s".', $name)); + throw new \InvalidArgumentException(\sprintf('Unable to find file "%s".', $name)); } public function getEnvironment(): string @@ -265,7 +250,7 @@ public function getProjectDir(): string $r = new \ReflectionObject($this); if (!is_file($dir = $r->getFileName())) { - throw new \LogicException(sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name)); + throw new \LogicException(\sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name)); } $dir = $rootDir = \dirname($dir); @@ -292,10 +277,14 @@ public function getContainer(): ContainerInterface /** * @internal + * + * @deprecated since Symfony 7.1, to be removed in 8.0 */ public function setAnnotatedClassCache(array $annotatedClasses): void { - file_put_contents(($this->warmupDir ?: $this->getBuildDir()).'/annotations.map', sprintf('warmupDir ?: $this->getBuildDir()).'/annotations.map', \sprintf('bundles = []; foreach ($this->registerBundles() as $bundle) { $name = $bundle->getName(); if (isset($this->bundles[$name])) { - throw new \LogicException(sprintf('Trying to register two bundles with the same name "%s".', $name)); + throw new \LogicException(\sprintf('Trying to register two bundles with the same name "%s".', $name)); } $this->bundles[$name] = $bundle; } @@ -356,10 +349,8 @@ protected function initializeBundles() * The extension point similar to the Bundle::build() method. * * Use this method to register compiler passes and manipulate the container during the building process. - * - * @return void */ - protected function build(ContainerBuilder $container) + protected function build(ContainerBuilder $container): void { } @@ -375,7 +366,7 @@ protected function getContainerClass(): string $class = str_replace('\\', '_', $class).ucfirst($this->environment).($this->debug ? 'Debug' : '').'Container'; if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) { - throw new \InvalidArgumentException(sprintf('The environment "%s" contains invalid characters, it can only contain characters allowed in PHP class names.', $this->environment)); + throw new \InvalidArgumentException(\sprintf('The environment "%s" contains invalid characters, it can only contain characters allowed in PHP class names.', $this->environment)); } return $class; @@ -396,14 +387,15 @@ protected function getContainerBaseClass(): string * * The built version of the service container is used when fresh, otherwise the * container is built. - * - * @return void */ - protected function initializeContainer() + protected function initializeContainer(): void { $class = $this->getContainerClass(); $buildDir = $this->warmupDir ?: $this->getBuildDir(); - $cache = new ConfigCache($buildDir.'/'.$class.'.php', $this->debug); + $skip = $_SERVER['SYMFONY_DISABLE_RESOURCE_TRACKING'] ?? ''; + $skip = filter_var($skip, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) ?? explode(',', $skip); + $cache = new ConfigCache($buildDir.'/'.$class.'.php', $this->debug, null, \is_array($skip) && ['*'] !== $skip ? $skip : ($skip ? [] : null)); + $cachePath = $cache->getPath(); // Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors @@ -542,7 +534,7 @@ protected function initializeContainer() $buildDir = $this->container->getParameter('kernel.build_dir'); $cacheDir = $this->container->getParameter('kernel.cache_dir'); - $preload = $this instanceof WarmableInterface ? (array) $this->warmUp($cacheDir, $buildDir) : []; + $preload = $this instanceof WarmableInterface ? $this->warmUp($cacheDir, $buildDir) : []; if ($this->container->has('cache_warmer')) { $cacheWarmer = $this->container->get('cache_warmer'); @@ -551,7 +543,7 @@ protected function initializeContainer() $cacheWarmer->enableOptionalWarmers(); } - $preload = array_merge($preload, (array) $cacheWarmer->warmUp($cacheDir, $buildDir)); + $preload = array_merge($preload, $cacheWarmer->warmUp($cacheDir, $buildDir)); } if ($preload && file_exists($preloadFile = $buildDir.'/'.$class.'.preload.php')) { @@ -561,6 +553,8 @@ protected function initializeContainer() /** * Returns the kernel parameters. + * + * @return array */ protected function getKernelParameters(): array { @@ -601,13 +595,13 @@ protected function getKernelParameters(): array */ protected function buildContainer(): ContainerBuilder { - 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)); + throw new \RuntimeException(\sprintf('Unable to create the "%s" directory (%s).', $name, $dir)); } } elseif (!is_writable($dir)) { - throw new \RuntimeException(sprintf('Unable to write in the "%s" directory (%s).', $name, $dir)); + throw new \RuntimeException(\sprintf('Unable to write in the "%s" directory (%s).', $name, $dir)); } } @@ -616,17 +610,13 @@ protected function buildContainer(): ContainerBuilder $this->prepareContainer($container); $this->registerContainerConfiguration($this->getContainerLoader($container)); - $container->addCompilerPass(new AddAnnotatedClassesToCachePass($this)); - return $container; } /** * Prepares the ContainerBuilder before it is compiled. - * - * @return void */ - protected function prepareContainer(ContainerBuilder $container) + protected function prepareContainer(ContainerBuilder $container): void { $extensions = []; foreach ($this->bundles as $bundle) { @@ -676,10 +666,8 @@ protected function getContainerBuilder(): ContainerBuilder * * @param string $class The name of the class to generate * @param string $baseClass The name of the container's base class - * - * @return void */ - protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, string $class, string $baseClass) + protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, string $class, string $baseClass): void { // cache the container $dumper = new PhpDumper($container); @@ -691,30 +679,14 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container } } - $inlineFactories = false; - if (isset($buildParameters['.container.dumper.inline_factories'])) { - $inlineFactories = $buildParameters['.container.dumper.inline_factories']; - } elseif ($container->hasParameter('container.dumper.inline_factories')) { - trigger_deprecation('symfony/http-kernel', '6.3', 'Parameter "%s" is deprecated, use ".%1$s" instead.', 'container.dumper.inline_factories'); - $inlineFactories = $container->getParameter('container.dumper.inline_factories'); - } - - $inlineClassLoader = $this->debug; - if (isset($buildParameters['.container.dumper.inline_class_loader'])) { - $inlineClassLoader = $buildParameters['.container.dumper.inline_class_loader']; - } elseif ($container->hasParameter('container.dumper.inline_class_loader')) { - trigger_deprecation('symfony/http-kernel', '6.3', 'Parameter "%s" is deprecated, use ".%1$s" instead.', 'container.dumper.inline_class_loader'); - $inlineClassLoader = $container->getParameter('container.dumper.inline_class_loader'); - } - $content = $dumper->dump([ 'class' => $class, 'base_class' => $baseClass, 'file' => $cache->getPath(), 'as_files' => true, 'debug' => $this->debug, - 'inline_factories' => $inlineFactories, - 'inline_class_loader' => $inlineClassLoader, + 'inline_factories' => $buildParameters['.container.dumper.inline_factories'] ?? false, + 'inline_class_loader' => $buildParameters['.container.dumper.inline_class_loader'] ?? $this->debug, 'build_time' => $container->hasParameter('kernel.container_build_time') ? $container->getParameter('kernel.container_build_time') : time(), 'preload_classes' => array_map('get_class', $this->bundles), ]); @@ -774,79 +746,33 @@ private function preBoot(): ContainerInterface $container = $this->container; if ($container->hasParameter('kernel.trusted_hosts') && $trustedHosts = $container->getParameter('kernel.trusted_hosts')) { - Request::setTrustedHosts($trustedHosts); + Request::setTrustedHosts(\is_array($trustedHosts) ? $trustedHosts : preg_split('/\s*+,\s*+(?![^{]*})/', $trustedHosts)); } if ($container->hasParameter('kernel.trusted_proxies') && $container->hasParameter('kernel.trusted_headers') && $trustedProxies = $container->getParameter('kernel.trusted_proxies')) { - Request::setTrustedProxies(\is_array($trustedProxies) ? $trustedProxies : array_map('trim', explode(',', $trustedProxies)), $container->getParameter('kernel.trusted_headers')); - } + $trustedHeaders = $container->getParameter('kernel.trusted_headers'); - return $container; - } + if (\is_string($trustedHeaders)) { + $trustedHeaders = array_map('trim', explode(',', $trustedHeaders)); + } - /** - * Removes comments from a PHP source string. - * - * We don't use the PHP php_strip_whitespace() function - * as we want the content to be readable and well-formatted. - * - * @deprecated since Symfony 6.4 without replacement - */ - public static function stripComments(string $source): string - { - trigger_deprecation('symfony/http-kernel', '6.4', 'Method "%s()" is deprecated without replacement.', __METHOD__); - - if (!\function_exists('token_get_all')) { - return $source; - } - - $rawChunk = ''; - $output = ''; - $tokens = token_get_all($source); - $ignoreSpace = false; - for ($i = 0; isset($tokens[$i]); ++$i) { - $token = $tokens[$i]; - if (!isset($token[1]) || 'b"' === $token) { - $rawChunk .= $token; - } elseif (\T_START_HEREDOC === $token[0]) { - $output .= $rawChunk.$token[1]; - do { - $token = $tokens[++$i]; - $output .= isset($token[1]) && 'b"' !== $token ? $token[1] : $token; - } while (\T_END_HEREDOC !== $token[0]); - $rawChunk = ''; - } elseif (\T_WHITESPACE === $token[0]) { - if ($ignoreSpace) { - $ignoreSpace = false; - - continue; - } + if (\is_array($trustedHeaders)) { + $trustedHeaderSet = 0; - // replace multiple new lines with a single newline - $rawChunk .= preg_replace(['/\n{2,}/S'], "\n", $token[1]); - } elseif (\in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT])) { - if (!\in_array($rawChunk[\strlen($rawChunk) - 1], [' ', "\n", "\r", "\t"], true)) { - $rawChunk .= ' '; + foreach ($trustedHeaders as $header) { + if (!\defined($const = Request::class.'::HEADER_'.strtr(strtoupper($header), '-', '_'))) { + throw new \InvalidArgumentException(\sprintf('The trusted header "%s" is not supported.', $header)); + } + $trustedHeaderSet |= \constant($const); } - $ignoreSpace = true; } else { - $rawChunk .= $token[1]; - - // The PHP-open tag already has a new-line - if (\T_OPEN_TAG === $token[0]) { - $ignoreSpace = true; - } else { - $ignoreSpace = false; - } + $trustedHeaderSet = $trustedHeaders ?? (Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); } - } - $output .= $rawChunk; - - unset($tokens, $rawChunk); - gc_mem_caches(); + Request::setTrustedProxies(\is_array($trustedProxies) ? $trustedProxies : array_map('trim', explode(',', $trustedProxies)), $trustedHeaderSet); + } - return $output; + return $container; } public function __sleep(): array @@ -854,10 +780,7 @@ public function __sleep(): array return ['environment', 'debug']; } - /** - * @return void - */ - public function __wakeup() + public function __wakeup(): void { if (\is_object($this->environment) || \is_object($this->debug)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); diff --git a/Log/DebugLoggerInterface.php b/Log/DebugLoggerInterface.php index 956abc6f36..50bb08d7f5 100644 --- a/Log/DebugLoggerInterface.php +++ b/Log/DebugLoggerInterface.php @@ -33,19 +33,15 @@ interface DebugLoggerInterface * timestamp_rfc3339: string, * }> */ - public function getLogs(?Request $request = null); + public function getLogs(?Request $request = null): array; /** * Returns the number of errors. - * - * @return int */ - public function countErrors(?Request $request = null); + public function countErrors(?Request $request = null): int; /** * Removes all log records. - * - * @return void */ - public function clear(); + public function clear(): void; } diff --git a/Log/Logger.php b/Log/Logger.php index 50578a25e7..93c1dbc95b 100644 --- a/Log/Logger.php +++ b/Log/Logger.php @@ -74,13 +74,13 @@ public function __construct(?string $minLevel = null, $output = null, ?callable } if (!isset(self::LEVELS[$minLevel])) { - throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel)); + throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $minLevel)); } $this->minLevelIndex = self::LEVELS[$minLevel]; $this->formatter = null !== $formatter ? $formatter(...) : $this->format(...); if ($output && false === $this->handle = \is_string($output) ? @fopen($output, 'a') : $output) { - throw new InvalidArgumentException(sprintf('Unable to open "%s".', $output)); + throw new InvalidArgumentException(\sprintf('Unable to open "%s".', $output)); } $this->debug = $debug; } @@ -93,7 +93,7 @@ public function enableDebug(): void public function log($level, $message, array $context = []): void { if (!isset(self::LEVELS[$level])) { - throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); + throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $level)); } if (self::LEVELS[$level] < $this->minLevelIndex) { @@ -155,7 +155,7 @@ private function format(string $level, string $message, array $context, bool $pr $message = strtr($message, $replacements); } - $log = sprintf('[%s] %s', $level, $message); + $log = \sprintf('[%s] %s', $level, $message); if ($prefixDate) { $log = date(\DateTimeInterface::RFC3339).' '.$log; } diff --git a/Profiler/FileProfilerStorage.php b/Profiler/FileProfilerStorage.php index d2372c30e3..cc1fcc5335 100644 --- a/Profiler/FileProfilerStorage.php +++ b/Profiler/FileProfilerStorage.php @@ -33,21 +33,17 @@ class FileProfilerStorage implements ProfilerStorageInterface public function __construct(string $dsn) { if (!str_starts_with($dsn, 'file:')) { - throw new \RuntimeException(sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn)); + throw new \RuntimeException(\sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn)); } $this->folder = substr($dsn, 5); if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) { - throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder)); + throw new \RuntimeException(\sprintf('Unable to create the storage directory (%s).', $this->folder)); } } - /** - * @param \Closure|null $filter A filter to apply on the list of tokens - */ - public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null, ?string $statusCode = null/* , \Closure $filter = null */): array + public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null, ?string $statusCode = null, ?\Closure $filter = null): array { - $filter = 7 < \func_num_args() ? func_get_arg(7) : null; $file = $this->getIndexFilename(); if (!file_exists($file)) { @@ -78,11 +74,11 @@ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?i continue; } - if (!empty($start) && $csvTime < $start) { + if ($start && $csvTime < $start) { continue; } - if (!empty($end) && $csvTime > $end) { + if ($end && $csvTime > $end) { continue; } @@ -109,10 +105,7 @@ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?i return array_values($result); } - /** - * @return void - */ - public function purge() + public function purge(): void { $flags = \FilesystemIterator::SKIP_DOTS; $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); @@ -144,7 +137,7 @@ public function write(Profile $profile): bool // Create directory $dir = \dirname($file); if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) { - throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to create the storage directory (%s).', $dir)); } } @@ -269,10 +262,7 @@ protected function readLineFromFile($file): mixed return '' === $line ? null : $line; } - /** - * @return Profile - */ - protected function createProfileFromData(string $token, array $data, ?Profile $parent = null) + protected function createProfileFromData(string $token, array $data, ?Profile $parent = null): Profile { $profile = new Profile($token); $profile->setIp($data['ip']); diff --git a/Profiler/Profile.php b/Profiler/Profile.php index 08e7b65a21..84d9ab93ac 100644 --- a/Profiler/Profile.php +++ b/Profiler/Profile.php @@ -20,8 +20,6 @@ */ class Profile { - private string $token; - /** * @var DataCollectorInterface[] */ @@ -40,15 +38,12 @@ class Profile */ private array $children = []; - public function __construct(string $token) - { - $this->token = $token; + public function __construct( + private string $token, + ) { } - /** - * @return void - */ - public function setToken(string $token) + public function setToken(string $token): void { $this->token = $token; } @@ -63,10 +58,8 @@ public function getToken(): string /** * Sets the parent token. - * - * @return void */ - public function setParent(self $parent) + public function setParent(self $parent): void { $this->parent = $parent; } @@ -95,10 +88,7 @@ public function getIp(): ?string return $this->ip; } - /** - * @return void - */ - public function setIp(?string $ip) + public function setIp(?string $ip): void { $this->ip = $ip; } @@ -111,10 +101,7 @@ public function getMethod(): ?string return $this->method; } - /** - * @return void - */ - public function setMethod(string $method) + public function setMethod(string $method): void { $this->method = $method; } @@ -127,10 +114,7 @@ public function getUrl(): ?string return $this->url; } - /** - * @return void - */ - public function setUrl(?string $url) + public function setUrl(?string $url): void { $this->url = $url; } @@ -140,18 +124,12 @@ public function getTime(): int return $this->time ?? 0; } - /** - * @return void - */ - public function setTime(int $time) + public function setTime(int $time): void { $this->time = $time; } - /** - * @return void - */ - public function setStatusCode(int $statusCode) + public function setStatusCode(int $statusCode): void { $this->statusCode = $statusCode; } @@ -191,10 +169,8 @@ public function getChildren(): array * Sets children profiler. * * @param Profile[] $children - * - * @return void */ - public function setChildren(array $children) + public function setChildren(array $children): void { $this->children = []; foreach ($children as $child) { @@ -204,10 +180,8 @@ public function setChildren(array $children) /** * Adds the child token. - * - * @return void */ - public function addChild(self $child) + public function addChild(self $child): void { $this->children[] = $child; $child->setParent($this); @@ -232,7 +206,7 @@ public function getChildByToken(string $token): ?self public function getCollector(string $name): DataCollectorInterface { if (!isset($this->collectors[$name])) { - throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + throw new \InvalidArgumentException(\sprintf('Collector "%s" does not exist.', $name)); } return $this->collectors[$name]; @@ -252,10 +226,8 @@ public function getCollectors(): array * Sets the Collectors associated with this profile. * * @param DataCollectorInterface[] $collectors - * - * @return void */ - public function setCollectors(array $collectors) + public function setCollectors(array $collectors): void { $this->collectors = []; foreach ($collectors as $collector) { @@ -265,10 +237,8 @@ public function setCollectors(array $collectors) /** * Adds a Collector. - * - * @return void */ - public function addCollector(DataCollectorInterface $collector) + public function addCollector(DataCollectorInterface $collector): void { $this->collectors[$collector->getName()] = $collector; } diff --git a/Profiler/Profiler.php b/Profiler/Profiler.php index fd5b28531e..b2089f10c7 100644 --- a/Profiler/Profiler.php +++ b/Profiler/Profiler.php @@ -26,40 +26,33 @@ */ class Profiler implements ResetInterface { - private ProfilerStorageInterface $storage; - /** * @var DataCollectorInterface[] */ private array $collectors = []; - private ?LoggerInterface $logger; private bool $initiallyEnabled = true; - private bool $enabled = true; - public function __construct(ProfilerStorageInterface $storage, ?LoggerInterface $logger = null, bool $enable = true) - { - $this->storage = $storage; - $this->logger = $logger; - $this->initiallyEnabled = $this->enabled = $enable; + public function __construct( + private ProfilerStorageInterface $storage, + private ?LoggerInterface $logger = null, + private bool $enabled = true, + ) { + $this->initiallyEnabled = $enabled; } /** * Disables the profiler. - * - * @return void */ - public function disable() + public function disable(): void { $this->enabled = false; } /** * Enables the profiler. - * - * @return void */ - public function enable() + public function enable(): void { $this->enabled = true; } @@ -110,10 +103,8 @@ public function saveProfile(Profile $profile): bool /** * Purges all data from the storage. - * - * @return void */ - public function purge() + public function purge(): void { $this->storage->purge(); } @@ -128,10 +119,8 @@ public function purge() * * @see https://php.net/datetime.formats for the supported date/time formats */ - public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?string $start, ?string $end, ?string $statusCode = null/* , \Closure $filter = null */): array + public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?string $start, ?string $end, ?string $statusCode = null, ?\Closure $filter = null): array { - $filter = 7 < \func_num_args() ? func_get_arg(7) : null; - return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end), $statusCode, $filter); } @@ -144,7 +133,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep return null; } - $profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $profile = new Profile(bin2hex(random_bytes(3))); $profile->setTime(time()); $profile->setUrl($request->getUri()); $profile->setMethod($request->getMethod()); @@ -175,10 +164,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep return $profile; } - /** - * @return void - */ - public function reset() + public function reset(): void { foreach ($this->collectors as $collector) { $collector->reset(); @@ -198,10 +184,8 @@ public function all(): array * Sets the Collectors associated with this profiler. * * @param DataCollectorInterface[] $collectors An array of collectors - * - * @return void */ - public function set(array $collectors = []) + public function set(array $collectors = []): void { $this->collectors = []; foreach ($collectors as $collector) { @@ -211,10 +195,8 @@ public function set(array $collectors = []) /** * Adds a Collector. - * - * @return void */ - public function add(DataCollectorInterface $collector) + public function add(DataCollectorInterface $collector): void { $this->collectors[$collector->getName()] = $collector; } @@ -239,7 +221,7 @@ public function has(string $name): bool public function get(string $name): DataCollectorInterface { if (!isset($this->collectors[$name])) { - throw new \InvalidArgumentException(sprintf('Collector "%s" does not exist.', $name)); + throw new \InvalidArgumentException(\sprintf('Collector "%s" does not exist.', $name)); } return $this->collectors[$name]; 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/Profiler/ProfilerStorageInterface.php b/Profiler/ProfilerStorageInterface.php index e2a25bc993..eed353d171 100644 --- a/Profiler/ProfilerStorageInterface.php +++ b/Profiler/ProfilerStorageInterface.php @@ -35,7 +35,7 @@ interface ProfilerStorageInterface * @param string|null $statusCode The response status code * @param \Closure|null $filter A filter to apply on the list of tokens */ - public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null/* , string $statusCode = null, \Closure $filter = null */): array; + public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null, ?string $statusCode = null, ?\Closure $filter = null): array; /** * Reads data associated with the given token. @@ -51,8 +51,6 @@ public function write(Profile $profile): bool; /** * Purges all data from the database. - * - * @return void */ - public function purge(); + public function purge(): void; } diff --git a/RebootableInterface.php b/RebootableInterface.php index e973f55400..3c4029f589 100644 --- a/RebootableInterface.php +++ b/RebootableInterface.php @@ -25,8 +25,6 @@ interface RebootableInterface * while building the container. Use the %kernel.build_dir% parameter instead. * * @param string|null $warmupDir pass null to reboot in the regular build directory - * - * @return void */ - public function reboot(?string $warmupDir); + public function reboot(?string $warmupDir): void; } diff --git a/Resources/welcome.html.php b/Resources/welcome.html.php index 03453c5c75..549c4ff192 100644 --- a/Resources/welcome.html.php +++ b/Resources/welcome.html.php @@ -1,124 +1,342 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +$renderSymfonyLogoSvg = << +SVG; + +// SVG icons from the Tabler Icons project +// MIT License - Copyright (c) 2020-2023 Paweł Kuna +// https://github.com/tabler/tabler-icons/blob/master/LICENSE + +$renderBoxIconSvg = << +SVG; + +$renderFolderIconSvg = << +SVG; + +$renderInfoIconSvg = << +SVG; + +$renderNextStepIconSvg = << +SVG; + +$renderLearnIconSvg = << +SVG; + +$renderCommunityIconSvg = << +SVG; + +$renderUpdatesIconSvg = << +SVG; + +$renderWavesSvg = << +SVG; +?> - + Welcome to Symfony! -
-
- -

- You're seeing this page because you haven't configured any homepage URL and debug mode is enabled. -

-
- -
-
- -

Welcome to Symfony

-
+
+
+ + +
+
    +
  • + + You are using Symfony version +
  • + +
  • + + Your application is ready at: +
  • + +
  • + + You are seeing this page because the homepage URL is not configured and debug mode is enabled. +
  • +
-
- - - - - - -

Your application is now ready and you can start working on it.

+

+ Next Step + + + Create your first page + to replace this placeholder page. + +

+
- -
- -
-
- - - + + + +
+
+
-
-
+ diff --git a/TerminableInterface.php b/TerminableInterface.php index 341ef73c4d..5b3e186042 100644 --- a/TerminableInterface.php +++ b/TerminableInterface.php @@ -27,8 +27,6 @@ interface TerminableInterface * Terminates a request/response cycle. * * Should be called after sending the response and before shutting down the kernel. - * - * @return void */ - public function terminate(Request $request, Response $response); + public function terminate(Request $request, Response $response): void; } diff --git a/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php b/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php index 9dc6a08312..6b0f4027ff 100644 --- a/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php @@ -22,20 +22,13 @@ class BackedEnumValueResolverTest extends TestCase { /** - * In Symfony 7, keep this test case but remove the call to supports(). - * - * @group legacy - * * @dataProvider provideTestSupportsData */ public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport) { $resolver = new BackedEnumValueResolver(); - if (!$expectedSupport) { - $this->assertSame([], $resolver->resolve($request, $metadata)); - } - self::assertSame($expectedSupport, $resolver->supports($request, $metadata)); + $this->assertCount((int) $expectedSupport, $resolver->resolve($request, $metadata)); } public static function provideTestSupportsData(): iterable diff --git a/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php b/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php index 6529ca9f76..636c811f98 100644 --- a/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php @@ -50,26 +50,6 @@ public static function getClasses() yield [FooDateTime::class]; } - /** - * @group legacy - */ - public function testSupports() - { - $resolver = new DateTimeValueResolver(); - - $argument = new ArgumentMetadata('dummy', \DateTime::class, false, false, null); - $request = self::requestWithAttributes(['dummy' => 'now']); - $this->assertTrue($resolver->supports($request, $argument)); - - $argument = new ArgumentMetadata('dummy', FooDateTime::class, false, false, null); - $request = self::requestWithAttributes(['dummy' => 'now']); - $this->assertTrue($resolver->supports($request, $argument)); - - $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); - $request = self::requestWithAttributes(['dummy' => 'now']); - $this->assertFalse($resolver->supports($request, $argument)); - } - public function testUnsupportedArgument() { $resolver = new DateTimeValueResolver(); diff --git a/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php b/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php index e28f8d5130..3fc74a1d70 100644 --- a/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php @@ -20,23 +20,6 @@ class NotTaggedControllerValueResolverTest extends TestCase { - /** - * @group legacy - */ - public function testDoSupportWhenControllerDoNotExists() - { - $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); - $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); - $request = $this->requestWithAttributes(['_controller' => 'my_controller']); - - $this->assertTrue($resolver->supports($request, $argument)); - } - - /** - * In Symfony 7, keep this test case but remove the call to supports(). - * - * @group legacy - */ public function testDoNotSupportWhenControllerExists() { $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([ @@ -47,21 +30,14 @@ public function testDoNotSupportWhenControllerExists() $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']); $this->assertSame([], $resolver->resolve($request, $argument)); - $this->assertFalse($resolver->supports($request, $argument)); } - /** - * In Symfony 7, keep this test case but remove the call to supports(). - * - * @group legacy - */ public function testDoNotSupportEmptyController() { $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => '']); $this->assertSame([], $resolver->resolve($request, $argument)); - $this->assertFalse($resolver->supports($request, $argument)); } public function testController() @@ -104,11 +80,6 @@ public function testControllerNameIsAnArray() $resolver->resolve($request, $argument); } - /** - * In Symfony 7, keep this test case but remove the call to supports(). - * - * @group legacy - */ public function testInvokableController() { $this->expectException(RuntimeException::class); @@ -116,7 +87,6 @@ public function testInvokableController() $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\Controller\Mine']); - $this->assertTrue($resolver->supports($request, $argument)); $resolver->resolve($request, $argument); } diff --git a/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php b/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php index 682421c7dd..2b887db821 100644 --- a/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php @@ -13,12 +13,16 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +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 { @@ -29,17 +33,45 @@ protected function setUp(): void $this->resolver = new QueryParameterValueResolver(); } + public function testSkipWhenNoAttribute() + { + $metadata = new ArgumentMetadata('firstName', 'string', false, true, false); + + $this->assertSame([], $this->resolver->resolve(Request::create('/'), $metadata)); + } + /** - * @dataProvider provideTestResolve + * @dataProvider validDataProvider */ - public function testResolve(Request $request, ArgumentMetadata $metadata, array $expected, ?string $exceptionClass = null, ?string $exceptionMessage = null) + public function testResolvingSuccessfully(Request $request, ArgumentMetadata $metadata, array $expected) { - if ($exceptionMessage) { - self::expectException($exceptionClass); - self::expectExceptionMessage($exceptionMessage); - } + $this->assertEquals($expected, $this->resolver->resolve($request, $metadata)); + } - self::assertSame($expected, $this->resolver->resolve($request, $metadata)); + /** + * @dataProvider invalidArgumentTypeProvider + */ + public function testResolvingWithInvalidArgumentType(Request $request, ArgumentMetadata $metadata, string $exceptionMessage) + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage($exceptionMessage); + + $this->resolver->resolve($request, $metadata); + } + + /** + * @dataProvider invalidOrMissingArgumentProvider + */ + public function testResolvingWithInvalidOrMissingArgument(Request $request, ArgumentMetadata $metadata, HttpException $expectedException) + { + try { + $this->resolver->resolve($request, $metadata); + + $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $exception) { + $this->assertSame($expectedException->getMessage(), $exception->getMessage()); + $this->assertSame($expectedException->getStatusCode(), $exception->getStatusCode()); + } } /** @@ -47,250 +79,256 @@ public function testResolve(Request $request, ArgumentMetadata $metadata, array * Request, * ArgumentMetadata, * array, - * null|class-string<\Exception>, - * null|string * }> */ - public static function provideTestResolve(): iterable + public static function validDataProvider(): iterable { yield 'parameter found and array' => [ Request::create('/', 'GET', ['ids' => ['1', '2']]), new ArgumentMetadata('ids', 'array', false, false, false, attributes: [new MapQueryParameter()]), [['1', '2']], - null, ]; + yield 'parameter found and array variadic' => [ Request::create('/', 'GET', ['ids' => [['1', '2'], ['2']]]), new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]), [['1', '2'], ['2']], - null, - ]; - yield 'parameter found and array variadic with parameter not array failure' => [ - Request::create('/', 'GET', ['ids' => [['1', '2'], 1]]), - new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "ids".', ]; + yield 'parameter found and string' => [ Request::create('/', 'GET', ['firstName' => 'John']), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]), ['John'], - null, ]; + yield 'parameter found and string variadic' => [ Request::create('/', 'GET', ['ids' => ['1', '2']]), new ArgumentMetadata('ids', 'string', true, false, false, attributes: [new MapQueryParameter()]), ['1', '2'], - null, ]; + 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'], - null, ]; + 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], - null, - ]; - yield 'parameter found and string with regexp filter that does not match' => [ - Request::create('/', 'GET', ['firstName' => 'Fabien']), - new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/John/'])]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "firstName".', ]; + 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'], - null, ]; + 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'], - null, - ]; - yield 'parameter found and string variadic with regexp filter that does not match' => [ - Request::create('/', 'GET', ['firstName' => ['Fabien']]), - new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/John/'])]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "firstName".', ]; + 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], - null, ]; + 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], - null, ]; + 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], - null, ]; + 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], - null, + [1099.0, 5.99], ]; + yield 'parameter found and boolean yes' => [ Request::create('/', 'GET', ['isVerified' => 'yes']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [true], - null, ]; + yield 'parameter found and boolean yes variadic' => [ Request::create('/', 'GET', ['isVerified' => ['yes', 'yes']]), new ArgumentMetadata('isVerified', 'bool', true, false, false, attributes: [new MapQueryParameter()]), [true, true], - null, ]; + yield 'parameter found and boolean true' => [ Request::create('/', 'GET', ['isVerified' => 'true']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [true], - null, ]; + yield 'parameter found and boolean 1' => [ Request::create('/', 'GET', ['isVerified' => '1']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [true], - null, ]; + yield 'parameter found and boolean no' => [ Request::create('/', 'GET', ['isVerified' => 'no']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [false], - null, - ]; - yield 'parameter found and boolean invalid' => [ - Request::create('/', 'GET', ['isVerified' => 'whatever']), - new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "isVerified".', ]; yield 'parameter found and backing value' => [ Request::create('/', 'GET', ['suit' => 'H']), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), [Suit::Hearts], - null, ]; + yield 'parameter found and backing value variadic' => [ Request::create('/', 'GET', ['suits' => ['H', 'D']]), new ArgumentMetadata('suits', Suit::class, true, false, false, attributes: [new MapQueryParameter()]), [Suit::Hearts, Suit::Diamonds], - null, - ]; - yield 'parameter found and backing value not int nor string' => [ - Request::create('/', 'GET', ['suit' => 1]), - new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "suit".', ]; + yield 'parameter found and backing value not int nor string that fallbacks to null on failure' => [ Request::create('/', 'GET', ['suit' => 1]), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL, flags: \FILTER_NULL_ON_FAILURE)]), [null], - null, - ]; - yield 'parameter found and value not valid backing value' => [ - Request::create('/', 'GET', ['suit' => 'B']), - new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "suit".', ]; + yield 'parameter found and value not valid backing value that falls back to null on failure' => [ Request::create('/', 'GET', ['suit' => 'B']), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]), [null], - null, - ]; - yield 'parameter found and backing type variadic and at least one backing value not int nor string' => [ - Request::create('/', 'GET', ['suits' => [1, 'D']]), - new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "suits".', ]; + 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], - null, - ]; - yield 'parameter found and backing type variadic and at least one value not valid backing value' => [ - Request::create('/', 'GET', ['suits' => ['B', 'D']]), - new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), - [], - NotFoundHttpException::class, - 'Invalid query parameter "suits".', ]; + yield 'parameter found and backing type variadic and at least one value not valid backing value that falls back to null on failure' => [ Request::create('/', 'GET', ['suits' => ['B', 'D']]), new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]), [null], - null, ]; yield 'parameter not found but nullable' => [ Request::create('/', 'GET'), new ArgumentMetadata('firstName', 'string', false, false, false, true, [new MapQueryParameter()]), [], - null, ]; yield 'parameter not found but optional' => [ Request::create('/', 'GET'), new ArgumentMetadata('firstName', 'string', false, true, false, attributes: [new MapQueryParameter()]), [], - null, ]; - yield 'parameter not found' => [ - Request::create('/', 'GET'), - new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]), - [], - NotFoundHttpException::class, - 'Missing query parameter "firstName".', + yield 'parameter found and ULID' => [ + Request::create('/', 'GET', ['groupId' => '01E439TP9XJZ9RPFH3T1PYBCR8']), + new ArgumentMetadata('groupId', Ulid::class, false, true, false, attributes: [new MapQueryParameter()]), + [Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8')], ]; + } + /** + * @return iterable + */ + public static function invalidArgumentTypeProvider(): iterable + { yield 'unsupported type' => [ Request::create('/', 'GET', ['standardClass' => 'test']), new ArgumentMetadata('standardClass', \stdClass::class, false, false, false, attributes: [new MapQueryParameter()]), - [], - \LogicException::class, - '#[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()]), - [], - \LogicException::class, - '#[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.', ]; } - public function testSkipWhenNoAttribute() + /** + * @return iterable + */ + public static function invalidOrMissingArgumentProvider(): iterable { - $metadata = new ArgumentMetadata('firstName', 'string', false, true, false); + yield 'parameter found and array variadic with parameter not array failure' => [ + Request::create('/', 'GET', ['ids' => [['1', '2'], '1']]), + new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]), + new NotFoundHttpException('Invalid query parameter "ids".'), + ]; + + yield 'parameter found and string with regexp filter that does not match' => [ + Request::create('/', 'GET', ['firstName' => 'Fabien']), + new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/John/'])]), + new NotFoundHttpException('Invalid query parameter "firstName".'), + ]; - self::assertSame([], $this->resolver->resolve(Request::create('/'), $metadata)); + yield 'parameter found and string variadic with regexp filter that does not match' => [ + Request::create('/', 'GET', ['firstName' => ['Fabien']]), + new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/John/'])]), + new NotFoundHttpException('Invalid query parameter "firstName".'), + ]; + + yield 'parameter found and boolean invalid' => [ + Request::create('/', 'GET', ['isVerified' => 'whatever']), + new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), + new NotFoundHttpException('Invalid query parameter "isVerified".'), + ]; + + yield 'parameter found and backing value not int nor string' => [ + Request::create('/', 'GET', ['suit' => 1]), + new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]), + new NotFoundHttpException('Invalid query parameter "suit".'), + ]; + + yield 'parameter found and value not valid backing value' => [ + Request::create('/', 'GET', ['suit' => 'B']), + new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), + new NotFoundHttpException('Invalid query parameter "suit".'), + ]; + + yield 'parameter found and backing type variadic and at least one backing value not int nor string' => [ + Request::create('/', 'GET', ['suits' => [1, 'D']]), + new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]), + new NotFoundHttpException('Invalid query parameter "suits".'), + ]; + + yield 'parameter found and backing type variadic and at least one value not valid backing value' => [ + Request::create('/', 'GET', ['suits' => ['B', 'D']]), + new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), + new NotFoundHttpException('Invalid query parameter "suits".'), + ]; + + yield 'parameter not found' => [ + Request::create('/', 'GET'), + new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]), + new NotFoundHttpException('Missing query parameter "firstName".'), + ]; + + yield 'parameter not found with custom validation failed status code' => [ + Request::create('/', 'GET'), + new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]), + new BadRequestHttpException('Missing query parameter "firstName".'), + ]; } } diff --git a/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 1b4c20672a..77cf7d9c58 100644 --- a/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -20,13 +20,14 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -116,7 +117,7 @@ public function testNullableValueArgument() $validator->expects($this->never()) ->method('validate'); - $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); + $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, true, [ MapRequestPayload::class => new MapRequestPayload(), @@ -138,9 +139,9 @@ public function testQueryNullableValueArgument() $validator->expects($this->never()) ->method('validate'); - $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); + $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator); - $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, true, [ + $argument = new ArgumentMetadata('valid', QueryPayload::class, false, false, null, true, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET'); @@ -160,7 +161,7 @@ public function testNullPayloadAndNotDefaultOrNullableArgument() $validator->expects($this->never()) ->method('validate'); - $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); + $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), @@ -173,9 +174,9 @@ public function testNullPayloadAndNotDefaultOrNullableArgument() 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) { - $this->assertSame(422, $e->getStatusCode()); + $this->assertSame(400, $e->getStatusCode()); } } @@ -185,9 +186,9 @@ public function testQueryNullPayloadAndNotDefaultOrNullableArgument() $validator->expects($this->never()) ->method('validate'); - $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); + $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()]), $validator); - $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ + $argument = new ArgumentMetadata('valid', QueryPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET'); @@ -198,7 +199,7 @@ public function testQueryNullPayloadAndNotDefaultOrNullableArgument() 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) { $this->assertSame(404, $e->getStatusCode()); } @@ -222,7 +223,7 @@ public function testWithoutValidatorAndCouldNotDenormalize() 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) { $this->assertInstanceOf(PartialDenormalizationException::class, $e->getPrevious()); } @@ -230,7 +231,7 @@ public function testWithoutValidatorAndCouldNotDenormalize() public function testValidationNotPassed() { - $content = '{"price": 50, "title": ["not a string"]}'; + $content = '{"price": 50.0, "title": ["not a string"]}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); @@ -250,12 +251,12 @@ public function testValidationNotPassed() 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->assertSame(422, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); - $this->assertSame(sprintf('This value should be of type %s.', class_exists(InvalidTypeException::class) ? 'string' : 'unknown'), $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); } } @@ -281,7 +282,7 @@ public function testValidationNotPerformedWhenPartialDenormalizationReturnsViola 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); @@ -306,7 +307,7 @@ public function testUnsupportedMedia() 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) { $this->assertSame(415, $e->getStatusCode()); } @@ -419,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); @@ -455,6 +456,40 @@ public function testRequestInputValidationPassed() $this->assertEquals([$payload], $event->getArguments()); } + public function testRequestArrayDenormalization() + { + $input = [ + ['price' => '50'], + ['price' => '23'], + ]; + $payload = [ + new RequestPayload(50), + new RequestPayload(23), + ]; + + $serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->once()) + ->method('validate') + ->willReturn(new ConstraintViolationList()); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class), + ]); + $request = Request::create('/', 'POST', $input); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + $resolver->onKernelControllerArguments($event); + + $this->assertEquals([$payload], $event->getArguments()); + } + public function testRequestInputTypeMismatch() { $input = ['price' => 'not a float']; @@ -479,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); @@ -487,6 +522,40 @@ public function testRequestInputTypeMismatch() } } + public function testItThrowsOnMissingAttributeType() + { + $serializer = new Serializer(); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + $request = Request::create('/', 'POST'); + $request->attributes->set('_controller', 'App\Controller\SomeController::someMethod'); + + $this->expectException(NearMissValueResolverException::class); + $this->expectExceptionMessage('Please set the $type argument of the #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload] attribute to the type of the objects in the expected array.'); + $resolver->resolve($request, $argument); + } + + public function testItThrowsOnInvalidAttributeTypeUsage() + { + $serializer = new Serializer(); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('prices', null, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class), + ]); + $request = Request::create('/', 'POST'); + $request->attributes->set('_controller', 'App\Controller\SomeController::someMethod'); + + $this->expectException(NearMissValueResolverException::class); + $this->expectExceptionMessage('Please set its type to "array" when using argument $type of #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload].'); + $resolver->resolve($request, $argument); + } + public function testItThrowsOnVariadicArgument() { $serializer = new Serializer(); @@ -588,7 +657,7 @@ public function testAcceptFormatNotPassed(mixed $acceptFormat, string $contentTy 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) { $this->assertSame(415, $e->getStatusCode()); $this->assertSame($expectedExceptionMessage, $e->getMessage()); @@ -684,7 +753,7 @@ public function testValidationGroupsNotPassed(string $method, ValueResolver $att 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); @@ -749,7 +818,7 @@ public function testQueryValidationErrorCustomStatusCode() 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->assertSame(400, $e->getStatusCode()); @@ -780,14 +849,117 @@ public function testRequestPayloadValidationErrorCustomStatusCode() 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->assertSame(400, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); - $this->assertSame(sprintf('This value should be of type %s.', class_exists(InvalidTypeException::class) ? 'string' : 'unknown'), $validationFailedException->getViolations()[0]->getMessage()); + $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); } } + + /** + * @dataProvider provideBoolArgument + */ + public function testBoolArgumentInQueryString(mixed $expectedValue, ?string $parameterValue) + { + $serializer = new Serializer([new ObjectNormalizer()]); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [ + MapQueryString::class => new MapQueryString(), + ]); + $request = Request::create('/', 'GET', ['value' => $parameterValue]); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + $resolver->onKernelControllerArguments($event); + + $this->assertSame($expectedValue, $event->getArguments()[0]->value); + } + + /** + * @dataProvider provideBoolArgument + */ + public function testBoolArgumentInBody(mixed $expectedValue, ?string $parameterValue) + { + $serializer = new Serializer([new ObjectNormalizer()]); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + $request = Request::create('/', 'POST', ['value' => $parameterValue], server: ['CONTENT_TYPE' => 'multipart/form-data']); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + $resolver->onKernelControllerArguments($event); + + $this->assertSame($expectedValue, $event->getArguments()[0]->value); + } + + public static function provideBoolArgument() + { + yield 'default value' => [null, null]; + yield '"0"' => [false, '0']; + yield '"false"' => [false, 'false']; + yield '"no"' => [false, 'no']; + yield '"off"' => [false, 'off']; + yield '"1"' => [true, '1']; + yield '"true"' => [true, 'true']; + yield '"yes"' => [true, 'yes']; + yield '"on"' => [true, 'on']; + } + + /** + * Boolean filtering must be disabled for content types other than form data. + */ + public function testBoolArgumentInJsonBody() + { + $serializer = new Serializer([new ObjectNormalizer()]); + $validator = $this->createMock(ValidatorInterface::class); + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + $request = Request::create('/', 'POST', ['value' => 'off'], server: ['CONTENT_TYPE' => 'application/json']); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + $resolver->onKernelControllerArguments($event); + + $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 @@ -831,3 +1003,10 @@ public function getPassword(): string return $this->password; } } + +class ObjectWithBoolArgument +{ + public function __construct(public readonly ?bool $value = null) + { + } +} diff --git a/Tests/Controller/ArgumentResolver/RequestValueResolverTest.php b/Tests/Controller/ArgumentResolver/RequestValueResolverTest.php new file mode 100644 index 0000000000..7d7e091d40 --- /dev/null +++ b/Tests/Controller/ArgumentResolver/RequestValueResolverTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\BrowserKit\Request as RandomRequest; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; + +class RequestValueResolverTest extends TestCase +{ + public function testSameRequestReturned() + { + $resolver = new RequestValueResolver(); + $expectedRequest = Request::create('/'); + $actualRequest = $resolver->resolve($expectedRequest, new ArgumentMetadata('request', Request::class, false, false, null)); + self::assertCount(1, $actualRequest); + self::assertSame($expectedRequest, $actualRequest[0] ?? null); + } + + public function testRequestIsNotResolvedForRandomClass() + { + $resolver = new RequestValueResolver(); + $expectedRequest = Request::create('/'); + $actualRequest = $resolver->resolve($expectedRequest, new ArgumentMetadata('request', self::class, false, false, null)); + self::assertCount(0, $actualRequest); + } + + public function testExceptionThrownForRandomRequestClass() + { + $resolver = new RequestValueResolver(); + $expectedRequest = Request::create('/'); + $this->expectException(NearMissValueResolverException::class); + $resolver->resolve($expectedRequest, new ArgumentMetadata('request', RandomRequest::class, false, false, null)); + } +} diff --git a/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php b/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php index 63a35b4124..a1a80fe82f 100644 --- a/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php @@ -13,20 +13,15 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; class ServiceValueResolverTest extends TestCase { - /** - * In Symfony 7, keep this test case but remove the call to supports(). - * - * @group legacy - */ public function testDoNotSupportWhenControllerDoNotExists() { $resolver = new ServiceValueResolver(new ServiceLocator([])); @@ -34,7 +29,6 @@ public function testDoNotSupportWhenControllerDoNotExists() $request = $this->requestWithAttributes(['_controller' => 'my_controller']); $this->assertSame([], $resolver->resolve($request, $argument)); - $this->assertFalse($resolver->supports($request, $argument)); } public function testExistingController() @@ -94,8 +88,8 @@ public function testControllerNameIsAnArray() public function testErrorIsTruncated() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Cannot autowire argument $dummy of "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.'); + $this->expectException(NearMissValueResolverException::class); + $this->expectExceptionMessage('Cannot autowire argument $dummy required by "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.'); $container = new ContainerBuilder(); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); diff --git a/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php b/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php index bf5c42f8c2..cf4e837f73 100644 --- a/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php @@ -14,28 +14,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Stopwatch\Stopwatch; class TraceableValueResolverTest extends TestCase { - /** - * @group legacy - */ - public function testTimingsInSupports() - { - $stopwatch = new Stopwatch(); - $resolver = new TraceableValueResolver(new ResolverStub(), $stopwatch); - $argument = new ArgumentMetadata('dummy', 'string', false, false, null); - $request = new Request(); - - $this->assertTrue($resolver->supports($request, $argument)); - - $event = $stopwatch->getEvent(ResolverStub::class.'::supports'); - $this->assertCount(1, $event->getPeriods()); - } - public function testTimingsInResolve() { $stopwatch = new Stopwatch(); @@ -48,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); @@ -64,13 +48,8 @@ public function testTimingsInResolve() } } -class ResolverStub implements ArgumentValueResolverInterface +class ResolverStub implements ValueResolverInterface { - public function supports(Request $request, ArgumentMetadata $argument): bool - { - return true; - } - public function resolve(Request $request, ArgumentMetadata $argument): iterable { yield 'first'; diff --git a/Tests/Controller/ArgumentResolver/UidValueResolverTest.php b/Tests/Controller/ArgumentResolver/UidValueResolverTest.php index cc43417508..1da4d976a2 100644 --- a/Tests/Controller/ArgumentResolver/UidValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/UidValueResolverTest.php @@ -25,19 +25,11 @@ class UidValueResolverTest extends TestCase { /** - * In Symfony 7, keep this test case but remove the call to supports(). - * - * @group legacy - * * @dataProvider provideSupports */ public function testSupports(bool $expected, Request $request, ArgumentMetadata $argument) { - if (!$expected) { - $this->assertSame([], (new UidValueResolver())->resolve($request, $argument)); - } - - $this->assertSame($expected, (new UidValueResolver())->supports($request, $argument)); + $this->assertCount((int) $expected, (new UidValueResolver())->resolve($request, $argument)); } public static function provideSupports() @@ -50,10 +42,8 @@ public static function provideSupports() 'Argument type is not a class' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', 'string', false, false, null)], 'Argument type is not a subclass of AbstractUid' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UlidFactory::class, false, false, null)], 'AbstractUid is not supported' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', AbstractUid::class, false, false, null)], - 'Custom abstract subclass is supported but will fail in resolve' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', TestAbstractCustomUid::class, false, false, null)], 'Known subclass' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UuidV4::class, false, false, null)], 'Format does not matter' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', Ulid::class, false, false, null)], - 'Custom subclass' => [true, new Request([], [], ['foo' => '01FPND7BD15ZV07X5VGDXAJ8VD']), new ArgumentMetadata('foo', TestCustomUid::class, false, false, null)], ]; } diff --git a/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php b/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php new file mode 100644 index 0000000000..91e28c864e --- /dev/null +++ b/Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php @@ -0,0 +1,403 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\ValidatorBuilder; + +class UploadedFileValueResolverTest extends TestCase +{ + private const FIXTURES_BASE_PATH = __DIR__.'/../../Fixtures/Controller/ArgumentResolver/UploadedFile'; + + /** + * @dataProvider provideContext + */ + public function testDefaults(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'foo', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + + /** @var UploadedFile $data */ + $data = $event->getArguments()[0]; + + $this->assertInstanceOf(UploadedFile::class, $data); + $this->assertSame('file-small.txt', $data->getFilename()); + $this->assertSame(36, $data->getSize()); + } + + /** + * @dataProvider provideContext + */ + public function testEmpty(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'qux', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + $data = $event->getArguments()[0]; + + $this->assertSame([], $data); + } + + /** + * @dataProvider provideContext + */ + public function testCustomName(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(name: 'bar'); + $argument = new ArgumentMetadata( + 'foo', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + + /** @var UploadedFile $data */ + $data = $event->getArguments()[0]; + + $this->assertInstanceOf(UploadedFile::class, $data); + $this->assertSame('file-big.txt', $data->getFilename()); + $this->assertSame(71, $data->getSize()); + } + + /** + * @dataProvider provideContext + */ + public function testConstraintsWithoutViolation(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 100)); + $argument = new ArgumentMetadata( + 'bar', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + + /** @var UploadedFile $data */ + $data = $event->getArguments()[0]; + + $this->assertInstanceOf(UploadedFile::class, $data); + $this->assertSame('file-big.txt', $data->getFilename()); + $this->assertSame(71, $data->getSize()); + } + + /** + * @dataProvider provideContext + */ + public function testConstraintsWithViolation(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); + $argument = new ArgumentMetadata( + 'bar', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + + $this->expectException(HttpException::class); + $this->expectExceptionMessageMatches('/^The file is too large/'); + + $resolver->onKernelControllerArguments($event); + } + + /** + * @dataProvider provideContext + */ + public function testMultipleFilesArray(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'baz', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + + /** @var UploadedFile[] $data */ + $data = $event->getArguments()[0]; + + $this->assertCount(2, $data); + $this->assertSame('file-small.txt', $data[0]->getFilename()); + $this->assertSame(36, $data[0]->getSize()); + $this->assertSame('file-big.txt', $data[1]->getFilename()); + $this->assertSame(71, $data[1]->getSize()); + } + + /** + * @dataProvider provideContext + */ + public function testMultipleFilesArrayConstraints(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); + $argument = new ArgumentMetadata( + 'baz', + UploadedFile::class, + false, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + + $this->expectException(HttpException::class); + $this->expectExceptionMessageMatches('/^The file is too large/'); + + $resolver->onKernelControllerArguments($event); + } + + /** + * @dataProvider provideContext + */ + public function testMultipleFilesVariadic(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'baz', + UploadedFile::class, + true, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + + /** @var UploadedFile[] $data */ + $data = $event->getArguments()[0]; + + $this->assertCount(2, $data); + $this->assertSame('file-small.txt', $data[0]->getFilename()); + $this->assertSame(36, $data[0]->getSize()); + $this->assertSame('file-big.txt', $data[1]->getFilename()); + $this->assertSame(71, $data[1]->getSize()); + } + + /** + * @dataProvider provideContext + */ + public function testMultipleFilesVariadicConstraints(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); + $argument = new ArgumentMetadata( + 'baz', + UploadedFile::class, + true, + false, + null, + false, + [$attribute::class => $attribute] + ); + $event = new ControllerArgumentsEvent( + $this->createMock(HttpKernelInterface::class), + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + + $this->expectException(HttpException::class); + $this->expectExceptionMessageMatches('/^The file is too large/'); + + $resolver->onKernelControllerArguments($event); + } + + /** + * @dataProvider provideContext + */ + public function testShouldAllowEmptyWhenNullable(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'qux', + UploadedFile::class, + false, + false, + null, + true, + [$attribute::class => $attribute] + ); + /** @var HttpKernelInterface&MockObject $httpKernel */ + $httpKernel = $this->createMock(HttpKernelInterface::class); + $event = new ControllerArgumentsEvent( + $httpKernel, + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + $data = $event->getArguments()[0]; + + $this->assertNull($data); + } + + /** + * @dataProvider provideContext + */ + public function testShouldAllowEmptyWhenHasDefaultValue(RequestPayloadValueResolver $resolver, Request $request) + { + $attribute = new MapUploadedFile(); + $argument = new ArgumentMetadata( + 'qux', + UploadedFile::class, + false, + true, + 'default-value', + false, + [$attribute::class => $attribute] + ); + /** @var HttpKernelInterface&MockObject $httpKernel */ + $httpKernel = $this->createMock(HttpKernelInterface::class); + $event = new ControllerArgumentsEvent( + $httpKernel, + static function () {}, + $resolver->resolve($request, $argument), + $request, + HttpKernelInterface::MAIN_REQUEST + ); + $resolver->onKernelControllerArguments($event); + $data = $event->getArguments()[0]; + + $this->assertSame('default-value', $data); + } + + public static function provideContext(): iterable + { + $resolver = new RequestPayloadValueResolver( + new Serializer(), + (new ValidatorBuilder())->getValidator() + ); + $small = new UploadedFile( + self::FIXTURES_BASE_PATH.'/file-small.txt', + 'file-small.txt', + 'text/plain', + null, + true + ); + $big = new UploadedFile( + self::FIXTURES_BASE_PATH.'/file-big.txt', + 'file-big.txt', + 'text/plain', + null, + true + ); + $request = Request::create( + '/', + 'POST', + files: [ + 'foo' => $small, + 'bar' => $big, + 'baz' => [$small, $big], + ], + server: ['HTTP_CONTENT_TYPE' => 'multipart/form-data'] + ); + + yield 'standard' => [$resolver, $request]; + } +} diff --git a/Tests/Controller/ArgumentResolverTest.php b/Tests/Controller/ArgumentResolverTest.php index 5d26396fbe..ed06130b92 100644 --- a/Tests/Controller/ArgumentResolverTest.php +++ b/Tests/Controller/ArgumentResolverTest.php @@ -21,10 +21,10 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingSession; @@ -178,31 +178,13 @@ public function testGetVariadicArgumentsWithoutArrayInRequest() self::getResolver()->getArguments($request, $controller); } - /** - * @group legacy - */ - public function testGetArgumentWithoutArray() - { - $this->expectException(\InvalidArgumentException::class); - $valueResolver = $this->createMock(ArgumentValueResolverInterface::class); - $resolver = self::getResolver([$valueResolver]); - - $valueResolver->expects($this->any())->method('supports')->willReturn(true); - $valueResolver->expects($this->any())->method('resolve')->willReturn([]); - - $request = Request::create('/'); - $request->attributes->set('foo', 'foo'); - $request->attributes->set('bar', 'foo'); - $controller = (new ArgumentResolverTestController())->controllerWithFooAndDefaultBar(...); - $resolver->getArguments($request, $controller); - } - public function testIfExceptionIsThrownWhenMissingAnArgument() { - $this->expectException(\RuntimeException::class); $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerWithFoo(...); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Controller "'.ArgumentResolverTestController::class.'::controllerWithFoo" requires the "$foo" argument that could not be resolved. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.'); self::getResolver()->getArguments($request, $controller); } @@ -365,6 +347,68 @@ public function testUnknownTargetedResolver() $this->expectException(ResolverNotFoundException::class); $resolver->getArguments($request, $controller); } + + public function testResolversChainCompletionWhenResolverThrowsSpecialException() + { + $failingValueResolver = new class implements ValueResolverInterface { + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + throw new NearMissValueResolverException('This resolver throws an exception'); + } + }; + // Put failing value resolver in the beginning + $expectedToCallValueResolver = $this->createMock(ValueResolverInterface::class); + $expectedToCallValueResolver->expects($this->once())->method('resolve')->willReturn([123]); + + $resolver = self::getResolver([$failingValueResolver, ...ArgumentResolver::getDefaultArgumentValueResolvers(), $expectedToCallValueResolver]); + $request = Request::create('/'); + $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; + + $actualArguments = $resolver->getArguments($request, $controller); + self::assertEquals([123], $actualArguments); + } + + public function testExceptionListSingle() + { + $failingValueResolverOne = new class implements ValueResolverInterface { + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + throw new NearMissValueResolverException('Some reason why value could not be resolved.'); + } + }; + + $resolver = self::getResolver([$failingValueResolverOne]); + $request = Request::create('/'); + $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolverTestController::controllerWithFoo" requires the "$foo" argument that could not be resolved. Some reason why value could not be resolved.'); + $resolver->getArguments($request, $controller); + } + + public function testExceptionListMultiple() + { + $failingValueResolverOne = new class implements ValueResolverInterface { + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + throw new NearMissValueResolverException('Some reason why value could not be resolved.'); + } + }; + $failingValueResolverTwo = new class implements ValueResolverInterface { + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + throw new NearMissValueResolverException('Another reason why value could not be resolved.'); + } + }; + + $resolver = self::getResolver([$failingValueResolverOne, $failingValueResolverTwo]); + $request = Request::create('/'); + $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolverTestController::controllerWithFoo" requires the "$foo" argument that could not be resolved. Possible reasons: 1) Some reason why value could not be resolved. 2) Another reason why value could not be resolved.'); + $resolver->getArguments($request, $controller); + } } class ArgumentResolverTestController @@ -432,13 +476,13 @@ public function controllerDisablingResolver(#[ValueResolver(RequestAttributeValu public function controllerTargetingManyResolvers( #[ValueResolver(RequestAttributeValueResolver::class)] #[ValueResolver(DefaultValueResolver::class)] - int $foo + int $foo, ) { } public function controllerTargetingUnknownResolver( #[ValueResolver('foo')] - int $bar + int $bar, ) { } } diff --git a/Tests/Controller/ContainerControllerResolverTest.php b/Tests/Controller/ContainerControllerResolverTest.php index 944f7ce29a..675ea5b298 100644 --- a/Tests/Controller/ContainerControllerResolverTest.php +++ b/Tests/Controller/ContainerControllerResolverTest.php @@ -23,16 +23,8 @@ public function testGetControllerService() { $service = new ControllerTestService('foo'); - $container = $this->createMockContainer(); - $container->expects($this->once()) - ->method('has') - ->with('foo') - ->willReturn(true); - $container->expects($this->once()) - ->method('get') - ->with('foo') - ->willReturn($service) - ; + $container = new Container(); + $container->set('foo', $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); @@ -48,17 +40,8 @@ public function testGetControllerInvokableService() { $service = new InvokableControllerService('bar'); - $container = $this->createMockContainer(); - $container->expects($this->once()) - ->method('has') - ->with('foo') - ->willReturn(true) - ; - $container->expects($this->once()) - ->method('get') - ->with('foo') - ->willReturn($service) - ; + $container = new Container(); + $container->set('foo', $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); @@ -73,17 +56,8 @@ public function testGetControllerInvokableServiceWithClassNameAsName() { $service = new InvokableControllerService('bar'); - $container = $this->createMockContainer(); - $container->expects($this->once()) - ->method('has') - ->with(InvokableControllerService::class) - ->willReturn(true) - ; - $container->expects($this->once()) - ->method('get') - ->with(InvokableControllerService::class) - ->willReturn($service) - ; + $container = new Container(); + $container->set(InvokableControllerService::class, $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); @@ -102,9 +76,8 @@ public function testInstantiateControllerWhenControllerStartsWithABackslash($con $service = new ControllerTestService('foo'); $class = ControllerTestService::class; - $container = $this->createMockContainer(); - $container->expects($this->once())->method('has')->with($class)->willReturn(true); - $container->expects($this->once())->method('get')->with($class)->willReturn($service); + $container = new Container(); + $container->set($class, $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); @@ -198,16 +171,11 @@ public static function getUndefinedControllers(): array protected function createControllerResolver(?LoggerInterface $logger = null, ?ContainerInterface $container = null) { if (!$container) { - $container = $this->createMockContainer(); + $container = new Container(); } return new ContainerControllerResolver($container, $logger); } - - protected function createMockContainer() - { - return $this->createMock(ContainerInterface::class); - } } class InvokableControllerService diff --git a/Tests/Controller/TraceableArgumentResolverTest.php b/Tests/Controller/TraceableArgumentResolverTest.php index 43bbb13e6b..74900f822e 100644 --- a/Tests/Controller/TraceableArgumentResolverTest.php +++ b/Tests/Controller/TraceableArgumentResolverTest.php @@ -28,8 +28,8 @@ public function testStopwatchEventIsStoppedWhenResolverThrows() $stopwatch = $this->createStub(Stopwatch::class); $stopwatch->method('start')->willReturn($stopwatchEvent); - $resolver = new class() implements ArgumentResolverInterface { - public function getArguments(Request $request, callable $controller): array + $resolver = new class implements ArgumentResolverInterface { + public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { throw new \Exception(); } diff --git a/Tests/Controller/TraceableControllerResolverTest.php b/Tests/Controller/TraceableControllerResolverTest.php index ecd4a23736..fcdb6bea97 100644 --- a/Tests/Controller/TraceableControllerResolverTest.php +++ b/Tests/Controller/TraceableControllerResolverTest.php @@ -28,7 +28,7 @@ public function testStopwatchEventIsStoppedWhenResolverThrows() $stopwatch = $this->createStub(Stopwatch::class); $stopwatch->method('start')->willReturn($stopwatchEvent); - $resolver = new class() implements ControllerResolverInterface { + $resolver = new class implements ControllerResolverInterface { public function getController(Request $request): callable|false { throw new \Exception(); diff --git a/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index 9c6fd03fb7..93fe699fcd 100644 --- a/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -32,12 +32,12 @@ protected function setUp(): void public function testSignature1() { - $arguments = $this->factory->createArgumentMetadata($this->signature1(...)); + $arguments = $this->factory->createArgumentMetadata([$this, 'signature1']); $this->assertEquals([ - new ArgumentMetadata('foo', self::class, false, false, null), - new ArgumentMetadata('bar', 'array', false, false, null), - new ArgumentMetadata('baz', 'callable', false, false, null), + new ArgumentMetadata('foo', self::class, false, false, null, controllerName: $this::class.'::signature1'), + new ArgumentMetadata('bar', 'array', false, false, null, controllerName: $this::class.'::signature1'), + new ArgumentMetadata('baz', 'callable', false, false, null, controllerName: $this::class.'::signature1'), ], $arguments); } @@ -46,9 +46,9 @@ public function testSignature2() $arguments = $this->factory->createArgumentMetadata($this->signature2(...)); $this->assertEquals([ - new ArgumentMetadata('foo', self::class, false, true, null, true), - new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, true, null, true), - new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null, true), + new ArgumentMetadata('foo', self::class, false, true, null, true, controllerName: $this::class.'::signature2'), + new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, true, null, true, controllerName: $this::class.'::signature2'), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null, true, controllerName: $this::class.'::signature2'), ], $arguments); } @@ -57,8 +57,8 @@ public function testSignature3() $arguments = $this->factory->createArgumentMetadata($this->signature3(...)); $this->assertEquals([ - new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, false, null), - new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null), + new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, false, null, controllerName: $this::class.'::signature3'), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null, controllerName: $this::class.'::signature3'), ], $arguments); } @@ -67,9 +67,9 @@ public function testSignature4() $arguments = $this->factory->createArgumentMetadata($this->signature4(...)); $this->assertEquals([ - new ArgumentMetadata('foo', null, false, true, 'default'), - new ArgumentMetadata('bar', null, false, true, 500), - new ArgumentMetadata('baz', null, false, true, []), + new ArgumentMetadata('foo', null, false, true, 'default', controllerName: $this::class.'::signature4'), + new ArgumentMetadata('bar', null, false, true, 500, controllerName: $this::class.'::signature4'), + new ArgumentMetadata('baz', null, false, true, [], controllerName: $this::class.'::signature4'), ], $arguments); } @@ -78,8 +78,8 @@ public function testSignature5() $arguments = $this->factory->createArgumentMetadata($this->signature5(...)); $this->assertEquals([ - new ArgumentMetadata('foo', 'array', false, true, null, true), - new ArgumentMetadata('bar', null, false, true, null, true), + new ArgumentMetadata('foo', 'array', false, true, null, true, controllerName: $this::class.'::signature5'), + new ArgumentMetadata('bar', null, false, true, null, true, controllerName: $this::class.'::signature5'), ], $arguments); } @@ -88,8 +88,8 @@ public function testVariadicSignature() $arguments = $this->factory->createArgumentMetadata([new VariadicController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('foo', null, false, false, null), - new ArgumentMetadata('bar', null, true, false, null), + new ArgumentMetadata('foo', null, false, false, null, controllerName: VariadicController::class.'::action'), + new ArgumentMetadata('bar', null, true, false, null, controllerName: VariadicController::class.'::action'), ], $arguments); } @@ -98,9 +98,9 @@ public function testBasicTypesSignature() $arguments = $this->factory->createArgumentMetadata([new BasicTypesController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('foo', 'string', false, false, null), - new ArgumentMetadata('bar', 'int', false, false, null), - new ArgumentMetadata('baz', 'float', false, false, null), + new ArgumentMetadata('foo', 'string', false, false, null, controllerName: BasicTypesController::class.'::action'), + new ArgumentMetadata('bar', 'int', false, false, null, controllerName: BasicTypesController::class.'::action'), + new ArgumentMetadata('baz', 'float', false, false, null, controllerName: BasicTypesController::class.'::action'), ], $arguments); } @@ -109,9 +109,9 @@ public function testNamedClosure() $arguments = $this->factory->createArgumentMetadata($this->signature1(...)); $this->assertEquals([ - new ArgumentMetadata('foo', self::class, false, false, null), - new ArgumentMetadata('bar', 'array', false, false, null), - new ArgumentMetadata('baz', 'callable', false, false, null), + new ArgumentMetadata('foo', self::class, false, false, null, controllerName: $this::class.'::signature1'), + new ArgumentMetadata('bar', 'array', false, false, null, controllerName: $this::class.'::signature1'), + new ArgumentMetadata('baz', 'callable', false, false, null, controllerName: $this::class.'::signature1'), ], $arguments); } @@ -120,10 +120,10 @@ public function testNullableTypesSignature() $arguments = $this->factory->createArgumentMetadata([new NullableController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('foo', 'string', false, false, null, true), - new ArgumentMetadata('bar', \stdClass::class, false, false, null, true), - new ArgumentMetadata('baz', 'string', false, true, 'value', true), - new ArgumentMetadata('last', 'string', false, true, '', false), + new ArgumentMetadata('foo', 'string', false, false, null, true, controllerName: NullableController::class.'::action'), + new ArgumentMetadata('bar', \stdClass::class, false, false, null, true, controllerName: NullableController::class.'::action'), + new ArgumentMetadata('baz', 'string', false, true, 'value', true, controllerName: NullableController::class.'::action'), + new ArgumentMetadata('last', 'string', false, true, '', false, controllerName: NullableController::class.'::action'), ], $arguments); } @@ -132,7 +132,7 @@ public function testAttributeSignature() $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']); $this->assertEquals([ - new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]), + new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], controllerName: AttributeController::class.'::action'), ], $arguments); } @@ -146,8 +146,8 @@ public function testIssue41478() { $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'issue41478']); $this->assertEquals([ - new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]), - new ArgumentMetadata('bat', 'string', false, false, null, false, []), + new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], controllerName: AttributeController::class.'::issue41478'), + new ArgumentMetadata('bat', 'string', false, false, null, false, [], controllerName: AttributeController::class.'::issue41478'), ], $arguments); } diff --git a/Tests/DataCollector/ConfigDataCollectorTest.php b/Tests/DataCollector/ConfigDataCollectorTest.php index efee6742ea..00da7a7801 100644 --- a/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/Tests/DataCollector/ConfigDataCollectorTest.php @@ -41,7 +41,7 @@ public function testCollect() $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), $c->hasZendOpcache()); $this->assertSame(\extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), $c->hasApcu()); - $this->assertSame(sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); + $this->assertSame(\sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); @@ -69,7 +69,7 @@ public function testCollectWithoutKernel() $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), $c->hasZendOpcache()); $this->assertSame(\extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), $c->hasApcu()); - $this->assertSame(sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); + $this->assertSame(\sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); 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/DataCollector/LoggerDataCollectorTest.php b/Tests/DataCollector/LoggerDataCollectorTest.php index 3e1654247b..0698dda774 100644 --- a/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/Tests/DataCollector/LoggerDataCollectorTest.php @@ -33,7 +33,7 @@ public function testCollectWithUnexpectedFormat() $c = new LoggerDataCollector($logger, __DIR__.'/'); $c->lateCollect(); - $compilerLogs = $c->getCompilerLogs()->getValue('message'); + $compilerLogs = $c->getCompilerLogs()->getValue(true); $this->assertSame([ ['message' => 'Removed service "Psr\Container\ContainerInterface"; reason: private alias.'], @@ -79,11 +79,11 @@ public function testCollectFromDeprecationsLog() $this->assertCount(1, $processedLogs); - $this->assertEquals($processedLogs[0]['type'], 'deprecation'); - $this->assertEquals($processedLogs[0]['errorCount'], 1); - $this->assertEquals($processedLogs[0]['timestamp'], (new \DateTimeImmutable())->setTimestamp(filemtime($path))->format(\DateTimeInterface::RFC3339_EXTENDED)); - $this->assertEquals($processedLogs[0]['priority'], 100); - $this->assertEquals($processedLogs[0]['priorityName'], 'DEBUG'); + $this->assertSame('deprecation', $processedLogs[0]['type']); + $this->assertSame(1, $processedLogs[0]['errorCount']); + $this->assertSame($processedLogs[0]['timestamp'], (new \DateTimeImmutable())->setTimestamp(filemtime($path))->format(\DateTimeInterface::RFC3339_EXTENDED)); + $this->assertSame(100, $processedLogs[0]['priority']); + $this->assertSame('DEBUG', $processedLogs[0]['priorityName']); $this->assertNull($processedLogs[0]['channel']); $this->assertInstanceOf(Data::class, $processedLogs[0]['message']); diff --git a/Tests/DataCollector/RequestDataCollectorTest.php b/Tests/DataCollector/RequestDataCollectorTest.php index 04649f9ef3..93ba4c1fc3 100644 --- a/Tests/DataCollector/RequestDataCollectorTest.php +++ b/Tests/DataCollector/RequestDataCollectorTest.php @@ -20,7 +20,6 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\SessionBagInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; @@ -89,7 +88,7 @@ public function testControllerInspection($name, $callable, $expected) $c->collect($request, $response); $c->lateCollect(); - $this->assertSame($expected, $c->getController()->getValue(true), sprintf('Testing: %s', $name)); + $this->assertSame($expected, $c->getController()->getValue(true), \sprintf('Testing: %s', $name)); } public static function provideControllerCallables(): array @@ -118,7 +117,7 @@ public static function provideControllerCallables(): array 'Closure', fn () => 'foo', [ - 'class' => \PHP_VERSION_ID >= 80400 ? sprintf('{closure:%s():%d}', __METHOD__, __LINE__ - 2) : __NAMESPACE__.'\{closure}', + 'class' => \PHP_VERSION_ID >= 80400 ? \sprintf('{closure:%s():%d}', __METHOD__, __LINE__ - 2) : __NAMESPACE__.'\{closure}', 'method' => null, 'file' => __FILE__, 'line' => __LINE__ - 5, @@ -301,7 +300,7 @@ public function testItCollectsTheSessionTraceProperly() $this->assertSame('getMetadataBag', $trace[0]['function']); $this->assertSame(self::class, $class = $trace[1]['class']); - $this->assertSame(sprintf('%s:%s', $class, $line), $usages[0]['name']); + $this->assertSame(\sprintf('%s:%s', $class, $line), $usages[0]['name']); } public function testStatelessCheck() @@ -412,7 +411,7 @@ private function getCookieByName(Response $response, $name) } } - throw new \InvalidArgumentException(sprintf('Cookie named "%s" is not in response', $name)); + throw new \InvalidArgumentException(\sprintf('Cookie named "%s" is not in response', $name)); } /** diff --git a/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php b/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php index ad8904902a..e57c349609 100644 --- a/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php +++ b/Tests/DependencyInjection/AddAnnotatedClassesToCachePassTest.php @@ -14,6 +14,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\DependencyInjection\AddAnnotatedClassesToCachePass; +/** + * @group legacy + */ class AddAnnotatedClassesToCachePassTest extends TestCase { public function testExpandClasses() @@ -33,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/LazyLoadingFragmentHandlerTest.php b/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php index c8db5e55e9..6ec35006c5 100644 --- a/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php +++ b/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -27,12 +27,11 @@ public function testRender() $renderer->expects($this->once())->method('getName')->willReturn('foo'); $renderer->expects($this->any())->method('render')->willReturn(new Response()); - $requestStack = $this->createMock(RequestStack::class); - $requestStack->expects($this->any())->method('getCurrentRequest')->willReturn(Request::create('/')); + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/')); - $container = $this->createMock(ContainerInterface::class); - $container->expects($this->once())->method('has')->with('foo')->willReturn(true); - $container->expects($this->once())->method('get')->willReturn($renderer); + $container = new Container(); + $container->set('foo', $renderer); $handler = new LazyLoadingFragmentHandler($container, $requestStack, false); diff --git a/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php b/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php index c22e05636a..35be55db7f 100644 --- a/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php +++ b/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php @@ -13,8 +13,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; use Symfony\Component\HttpKernel\Tests\Fixtures\AcmeFooBundle\AcmeFooBundle; @@ -48,7 +48,7 @@ public function testFooBundle() $configPass = new MergeExtensionConfigurationPass(['loaded', 'acme_foo']); $configPass->process($container); - $this->assertSame([[], ['bar' => 'baz']], $container->getExtensionConfig('loaded'), '->prependExtension() prepends an extension config'); + $this->assertSame([['bar' => 'baz'], []], $container->getExtensionConfig('loaded'), '->prependExtension() prepends an extension config'); $this->assertTrue($container->hasDefinition('acme_foo.foo'), '->loadExtension() registers a service'); $this->assertTrue($container->hasDefinition('acme_foo.bar'), '->loadExtension() imports a service'); $this->assertTrue($container->hasParameter('acme_foo.config'), '->loadExtension() sets a parameter'); diff --git a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 0a8c488edc..b281c873cb 100644 --- a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -144,6 +144,7 @@ public function testAllActions() $this->assertInstanceof(ServiceClosureArgument::class, $locator['foo::fooAction']); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $this->assertSame(ServiceLocator::class, $locator->getClass()); $this->assertFalse($locator->isPublic()); @@ -167,6 +168,7 @@ public function testExplicitArgument() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE))]; $this->assertEquals($expected, $locator->getArgument(0)); @@ -186,6 +188,7 @@ public function testOptionalArgument() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE))]; $this->assertEquals($expected, $locator->getArgument(0)); @@ -274,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() @@ -322,8 +325,8 @@ public function testBindings($bindingName) $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new Reference('foo'))]; $this->assertEquals($expected, $locator->getArgument(0)); @@ -388,7 +391,8 @@ public function testBindingsOnChildDefinitions() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertInstanceOf(ServiceClosureArgument::class, $locator['child::fooAction']); - $locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0])->getArgument(0); + $locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0); $this->assertInstanceOf(ServiceClosureArgument::class, $locator['someArg']); $this->assertEquals(new Reference('parent'), $locator['someArg']->getValues()[0]); } @@ -436,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() @@ -455,6 +459,7 @@ public function testBindWithTarget() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = [ 'apiKey' => new ServiceClosureArgument(new Reference('the_api_key')), @@ -475,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() @@ -505,7 +510,7 @@ public function testAutowireAttribute() $this->assertInstanceOf(\stdClass::class, $locator->get('serviceAsValue')); $this->assertInstanceOf(\stdClass::class, $locator->get('expressionAsValue')); $this->assertSame('bar', $locator->get('rawValue')); - $this->stringContains('Symfony_Component_HttpKernel_Tests_Fixtures_Suit_APP_SUIT', $locator->get('suit')); + $this->assertStringContainsString('Symfony_Component_HttpKernel_Tests_Fixtures_Suit_APP_SUIT', $locator->get('suit')); $this->assertSame('@bar', $locator->get('escapedRawValue')); $this->assertSame('foo', $locator->get('customAutowire')); $this->assertInstanceOf(FooInterface::class, $autowireCallable = $locator->get('autowireCallable')); @@ -514,6 +519,9 @@ public function testAutowireAttribute() $this->assertFalse($locator->has('service2')); } + /** + * @group legacy + */ public function testTaggedIteratorAndTaggedLocatorAttributes() { $container = new ContainerBuilder(); @@ -536,16 +544,12 @@ public function testTaggedIteratorAndTaggedLocatorAttributes() /** @var ServiceLocator $locator */ $locator = $container->get($locatorId)->get('foo::fooAction'); - $this->assertCount(6, $locator->getProvidedServices()); + $this->assertCount(2, $locator->getProvidedServices()); $this->assertTrue($locator->has('iterator1')); $this->assertInstanceOf(RewindableGenerator::class, $argIterator = $locator->get('iterator1')); $this->assertCount(2, $argIterator); - $this->assertTrue($locator->has('iterator2')); - $this->assertInstanceOf(RewindableGenerator::class, $argIterator = $locator->get('iterator2')); - $this->assertCount(2, $argIterator); - $this->assertTrue($locator->has('locator1')); $this->assertInstanceOf(ServiceLocator::class, $argLocator = $locator->get('locator1')); $this->assertCount(2, $argLocator); @@ -553,9 +557,38 @@ public function testTaggedIteratorAndTaggedLocatorAttributes() $this->assertTrue($argLocator->has('baz')); $this->assertSame(iterator_to_array($argIterator), [$argLocator->get('bar'), $argLocator->get('baz')]); + } + + public function testAutowireIteratorAndAutowireLocatorAttributes() + { + $container = new ContainerBuilder(); + $container->setParameter('some.parameter', 'bar'); + $resolver = $container->register('argument_resolver.service', \stdClass::class)->addArgument([]); + + $container->register('bar', \stdClass::class)->addTag('foobar'); + $container->register('baz', \stdClass::class)->addTag('foobar'); + + $container->register('foo', WithAutowireIteratorAndAutowireLocator::class) + ->addTag('controller.service_arguments'); + + (new RegisterControllerArgumentLocatorsPass())->process($container); - $this->assertTrue($locator->has('locator2')); - $this->assertInstanceOf(ServiceLocator::class, $argLocator = $locator->get('locator2')); + $locatorId = (string) $resolver->getArgument(0); + $container->getDefinition($locatorId)->setPublic(true); + + $container->compile(); + + /** @var ServiceLocator $locator */ + $locator = $container->get($locatorId)->get('foo::fooAction'); + + $this->assertCount(4, $locator->getProvidedServices()); + + $this->assertTrue($locator->has('iterator1')); + $this->assertInstanceOf(RewindableGenerator::class, $argIterator = $locator->get('iterator1')); + $this->assertCount(2, $argIterator); + + $this->assertTrue($locator->has('locator1')); + $this->assertInstanceOf(ServiceLocator::class, $argLocator = $locator->get('locator1')); $this->assertCount(2, $argLocator); $this->assertTrue($argLocator->has('bar')); $this->assertTrue($argLocator->has('baz')); @@ -679,7 +712,7 @@ public function fooAction( string $apiKey, #[Target('image.storage')] ControllerDummy $service1, - ControllerDummy $service2 + ControllerDummy $service2, ) { } } @@ -738,9 +771,16 @@ class WithTaggedIteratorAndTaggedLocator { public function fooAction( #[TaggedIterator('foobar')] iterable $iterator1, - #[AutowireIterator('foobar')] iterable $iterator2, #[TaggedLocator('foobar')] ServiceLocator $locator1, - #[AutowireLocator('foobar')] ServiceLocator $locator2, + ) { + } +} + +class WithAutowireIteratorAndAutowireLocator +{ + public function fooAction( + #[AutowireIterator('foobar')] iterable $iterator1, + #[AutowireLocator('foobar')] ServiceLocator $locator1, #[AutowireLocator(['bar', 'baz'])] ContainerInterface $container1, #[AutowireLocator(['foo' => new Autowire('%some.parameter%')])] ContainerInterface $container2, ) { diff --git a/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php b/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php index 8c99b882d3..21e0eb29ec 100644 --- a/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php +++ b/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php @@ -35,22 +35,23 @@ public function testProcess() $pass->process($container); $controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $getLocator = fn ($controllers, $k) => $container->getDefinition((string) $container->getDefinition((string) $controllers[$k]->getValues()[0])->getFactory()[0])->getArgument(0); - $this->assertCount(2, $container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0)); - $this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0)); - $this->assertCount(1, $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0)); + $this->assertCount(2, $getLocator($controllers, 'c1::fooAction')); + $this->assertCount(1, $getLocator($controllers, 'c2::setTestCase')); + $this->assertCount(1, $getLocator($controllers, 'c2::fooAction')); (new ResolveInvalidReferencesPass())->process($container); - $this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0)); - $this->assertSame([], $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0)); + $this->assertCount(1, $getLocator($controllers, 'c2::setTestCase')); + $this->assertSame([], $getLocator($controllers, 'c2::fooAction')); (new RemoveEmptyControllerArgumentLocatorsPass())->process($container); $controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame(['c1::fooAction', 'c1:fooAction'], array_keys($controllers)); - $this->assertSame(['bar'], array_keys($container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0))); + $this->assertSame(['bar'], array_keys($getLocator($controllers, 'c1::fooAction'))); $expectedLog = [ 'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing service-argument resolver for controller "c2::fooAction": no corresponding services exist for the referenced types.', diff --git a/Tests/Event/ControllerArgumentsEventTest.php b/Tests/Event/ControllerArgumentsEventTest.php index ef3ef7ad9e..0fc2013189 100644 --- a/Tests/Event/ControllerArgumentsEventTest.php +++ b/Tests/Event/ControllerArgumentsEventTest.php @@ -26,7 +26,7 @@ class ControllerArgumentsEventTest extends TestCase public function testControllerArgumentsEvent() { $event = new ControllerArgumentsEvent(new TestHttpKernel(), function () {}, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); - $this->assertEquals($event->getArguments(), ['test']); + $this->assertSame(['test'], $event->getArguments()); } public function testSetAttributes() diff --git a/Tests/EventListener/CacheAttributeListenerTest.php b/Tests/EventListener/CacheAttributeListenerTest.php index 0afae63a0a..d2c8ed0db6 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 testResponseKeepPublicIfConfigurationIsPublicTrueNoStoreTrue() + { + $request = $this->createRequest(new Cache(public: true, noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); + $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); + $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); + } + + public function testResponseKeepPrivateNoStoreIfConfigurationIsNoStoreTrue() + { + $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 testResponseIsPublicIfSharedMaxAgeSetAndNoStoreIsTrue() + { + $request = $this->createRequest(new Cache(smaxage: 1, noStore: true)); + + $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); + + $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); + $this->assertFalse($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()); } @@ -227,7 +274,7 @@ public function testEtagNotModifiedResponse(string $expression) $request = $this->createRequest(new Cache(etag: $expression)); $request->attributes->set('id', '12345'); - $request->headers->add(['If-None-Match' => sprintf('"%s"', hash('sha256', $entity->getId()))]); + $request->headers->add(['If-None-Match' => \sprintf('"%s"', hash('sha256', $entity->getId()))]); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), fn (TestEntity $test) => new Response(), [$entity], $request, null); @@ -330,7 +377,7 @@ private function createEventMock(Request $request, Response $response): Response private function getKernel(): MockObject&HttpKernelInterface { - return $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + return $this->createMock(HttpKernelInterface::class); } } diff --git a/Tests/EventListener/ErrorListenerTest.php b/Tests/EventListener/ErrorListenerTest.php index e9bc584048..7fdda59635 100644 --- a/Tests/EventListener/ErrorListenerTest.php +++ b/Tests/EventListener/ErrorListenerTest.php @@ -100,7 +100,7 @@ public function testHandleWithLogger($event, $event2) } $this->assertEquals(3, $logger->countErrors()); - $logs = $logger->getLogs('critical'); + $logs = $logger->getLogsForLevel('critical'); $this->assertCount(3, $logs); $this->assertStringStartsWith('Uncaught PHP Exception Exception: "foo" at ErrorListenerTest.php line', $logs[0]); $this->assertStringStartsWith('Uncaught PHP Exception Exception: "foo" at ErrorListenerTest.php line', $logs[1]); @@ -124,8 +124,8 @@ public function testHandleWithLoggerAndCustomConfiguration() $this->assertEquals(new Response('foo', 401), $event->getResponse()); $this->assertEquals(0, $logger->countErrors()); - $this->assertCount(0, $logger->getLogs('critical')); - $this->assertCount(1, $logger->getLogs('warning')); + $this->assertCount(0, $logger->getLogsForLevel('critical')); + $this->assertCount(1, $logger->getLogsForLevel('warning')); } public function testHandleWithLogLevelAttribute() @@ -139,8 +139,80 @@ public function testHandleWithLogLevelAttribute() $l->onKernelException($event); $this->assertEquals(0, $logger->countErrors()); - $this->assertCount(0, $logger->getLogs('critical')); - $this->assertCount(1, $logger->getLogs('warning')); + $this->assertCount(0, $logger->getLogsForLevel('critical')); + $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(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new ImplementingInterfaceWithLogLevelAttribute()); + $logger = new TestLogger(); + $l = new ErrorListener('not used', $logger); + + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertEquals(0, $logger->countErrors()); + $this->assertCount(0, $logger->getLogsForLevel('critical')); + $this->assertCount(1, $logger->getLogsForLevel('warning')); } public function testHandleWithLogLevelAttributeAndCustomConfiguration() @@ -158,8 +230,8 @@ public function testHandleWithLogLevelAttributeAndCustomConfiguration() $l->onKernelException($event); $this->assertEquals(0, $logger->countErrors()); - $this->assertCount(0, $logger->getLogs('warning')); - $this->assertCount(1, $logger->getLogs('info')); + $this->assertCount(0, $logger->getLogsForLevel('warning')); + $this->assertCount(1, $logger->getLogsForLevel('info')); } /** @@ -248,6 +320,19 @@ public function testCSPHeaderIsRemoved() $this->assertFalse($response->headers->has('content-security-policy'), 'CSP header has been removed'); } + public function testTerminating() + { + $listener = new ErrorListener('foo', $this->createMock(LoggerInterface::class)); + + $kernel = $this->createMock(HttpKernelInterface::class); + $kernel->expects($this->never())->method('handle'); + + $request = Request::create('/'); + + $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo'), true); + $listener->onKernelException($event); + } + /** * @dataProvider controllerProvider */ @@ -289,6 +374,7 @@ public static function exceptionWithAttributeProvider() yield [new WithCustomUserProvidedAttribute(), 208, ['name' => 'value']]; yield [new WithGeneralAttribute(), 412, ['some' => 'thing']]; yield [new ChildOfWithGeneralAttribute(), 412, ['some' => 'thing']]; + yield [new ImplementingInterfaceWithGeneralAttribute(), 412, ['some' => 'thing']]; } } @@ -298,6 +384,11 @@ public function countErrors(?Request $request = null): int { return \count($this->logs['critical']); } + + public function getLogs(?Request $request = null): array + { + return []; + } } class TestKernel implements HttpKernelInterface @@ -350,6 +441,20 @@ class WithGeneralAttribute extends \Exception { } +#[WithHttpStatus( + statusCode: Response::HTTP_PRECONDITION_FAILED, + headers: [ + 'some' => 'thing', + ] +)] +interface InterfaceWithGeneralAttribute +{ +} + +class ImplementingInterfaceWithGeneralAttribute extends \Exception implements InterfaceWithGeneralAttribute +{ +} + class ChildOfWithGeneralAttribute extends WithGeneralAttribute { } @@ -362,3 +467,12 @@ class WarningWithLogLevelAttribute extends \Exception class ChildOfWarningWithLogLevelAttribute extends WarningWithLogLevelAttribute { } + +#[WithLogLevel(LogLevel::WARNING)] +interface InterfaceWithLogLevelAttribute +{ +} + +class ImplementingInterfaceWithLogLevelAttribute extends \Exception implements InterfaceWithLogLevelAttribute +{ +} diff --git a/Tests/EventListener/LocaleListenerTest.php b/Tests/EventListener/LocaleListenerTest.php index 0ffa72fc3e..d7669aabc2 100644 --- a/Tests/EventListener/LocaleListenerTest.php +++ b/Tests/EventListener/LocaleListenerTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -26,16 +25,9 @@ class LocaleListenerTest extends TestCase { - private MockObject&RequestStack $requestStack; - - protected function setUp(): void - { - $this->requestStack = $this->createMock(RequestStack::class); - } - public function testIsAnEventSubscriber() { - $this->assertInstanceOf(EventSubscriberInterface::class, new LocaleListener($this->requestStack)); + $this->assertInstanceOf(EventSubscriberInterface::class, new LocaleListener(new RequestStack())); } public function testRegisteredEvent() @@ -51,7 +43,7 @@ public function testRegisteredEvent() public function testDefaultLocale() { - $listener = new LocaleListener($this->requestStack, 'fr'); + $listener = new LocaleListener(new RequestStack(), 'fr'); $event = $this->getEvent($request = Request::create('/')); $listener->setDefaultLocale($event); @@ -64,7 +56,7 @@ public function testLocaleFromRequestAttribute() $request->cookies->set(session_name(), 'value'); $request->attributes->set('_locale', 'es'); - $listener = new LocaleListener($this->requestStack, 'fr'); + $listener = new LocaleListener(new RequestStack(), 'fr'); $event = $this->getEvent($request); $listener->onKernelRequest($event); @@ -83,7 +75,7 @@ public function testLocaleSetForRoutingContext() $request = Request::create('/'); $request->attributes->set('_locale', 'es'); - $listener = new LocaleListener($this->requestStack, 'fr', $router); + $listener = new LocaleListener(new RequestStack(), 'fr', $router); $listener->onKernelRequest($this->getEvent($request)); } @@ -99,11 +91,15 @@ public function testRouterResetWithParentRequestOnKernelFinishRequest() $parentRequest = Request::create('/'); $parentRequest->setLocale('es'); - $this->requestStack->expects($this->once())->method('getParentRequest')->willReturn($parentRequest); + $requestStack = new RequestStack(); + $requestStack->push($parentRequest); + + $subRequest = new Request(); + $requestStack->push($subRequest); - $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST); + $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $subRequest, HttpKernelInterface::MAIN_REQUEST); - $listener = new LocaleListener($this->requestStack, 'fr', $router); + $listener = new LocaleListener($requestStack, 'fr', $router); $listener->onKernelFinishRequest($event); } @@ -111,7 +107,7 @@ public function testRequestLocaleIsNotOverridden() { $request = Request::create('/'); $request->setLocale('de'); - $listener = new LocaleListener($this->requestStack, 'fr'); + $listener = new LocaleListener(new RequestStack(), 'fr'); $event = $this->getEvent($request); $listener->onKernelRequest($event); @@ -123,7 +119,7 @@ public function testRequestPreferredLocaleFromAcceptLanguageHeader() $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); - $listener = new LocaleListener($this->requestStack, 'de', null, true, ['de', 'fr']); + $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['de', 'fr']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -134,7 +130,7 @@ public function testRequestPreferredLocaleFromAcceptLanguageHeader() public function testRequestDefaultLocaleIfNoAcceptLanguageHeaderIsPresent() { $request = new Request(); - $listener = new LocaleListener($this->requestStack, 'de', null, true, ['lt', 'de']); + $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['lt', 'de']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -145,7 +141,7 @@ public function testRequestDefaultLocaleIfNoAcceptLanguageHeaderIsPresent() public function testRequestVaryByLanguageAttributeIsSetIfUsingAcceptLanguageHeader() { $request = new Request(); - $listener = new LocaleListener($this->requestStack, 'de', null, true, ['lt', 'de']); + $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['lt', 'de']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -158,7 +154,7 @@ public function testRequestSecondPreferredLocaleFromAcceptLanguageHeader() $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); - $listener = new LocaleListener($this->requestStack, 'de', null, true, ['de', 'en']); + $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['de', 'en']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -171,7 +167,7 @@ public function testDontUseAcceptLanguageHeaderIfNotEnabled() $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); - $listener = new LocaleListener($this->requestStack, 'de', null, false, ['de', 'en']); + $listener = new LocaleListener(new RequestStack(), 'de', null, false, ['de', 'en']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -184,7 +180,7 @@ public function testRequestUnavailablePreferredLocaleFromAcceptLanguageHeader() $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); - $listener = new LocaleListener($this->requestStack, 'de', null, true, ['de', 'it']); + $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['de', 'it']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -197,7 +193,7 @@ public function testRequestNoLocaleFromAcceptLanguageHeader() $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); - $listener = new LocaleListener($this->requestStack, 'de', null, true); + $listener = new LocaleListener(new RequestStack(), 'de', null, true); $event = $this->getEvent($request); $listener->setDefaultLocale($event); @@ -211,7 +207,7 @@ public function testRequestAttributeLocaleNotOverriddenFromAcceptLanguageHeader( $request->attributes->set('_locale', 'it'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); - $listener = new LocaleListener($this->requestStack, 'de', null, true, ['fr', 'en']); + $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['fr', 'en']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); diff --git a/Tests/EventListener/RouterListenerTest.php b/Tests/EventListener/RouterListenerTest.php index 8c270a8e6e..f980984943 100644 --- a/Tests/EventListener/RouterListenerTest.php +++ b/Tests/EventListener/RouterListenerTest.php @@ -37,13 +37,6 @@ class RouterListenerTest extends TestCase { - private RequestStack $requestStack; - - protected function setUp(): void - { - $this->requestStack = $this->createMock(RequestStack::class); - } - /** * @dataProvider getPortData */ @@ -58,7 +51,7 @@ public function testPort($defaultHttpPort, $defaultHttpsPort, $uri, $expectedHtt ->method('getContext') ->willReturn($context); - $listener = new RouterListener($urlMatcher, $this->requestStack); + $listener = new RouterListener($urlMatcher, new RequestStack()); $event = $this->createRequestEventForUri($uri); $listener->onKernelRequest($event); @@ -98,7 +91,7 @@ public function testRequestMatcher() ->with($this->isInstanceOf(Request::class)) ->willReturn([]); - $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext()); + $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); } @@ -116,7 +109,7 @@ public function testSubRequestWithDifferentMethod() $context = new RequestContext(); - $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext()); + $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); // sub-request with another HTTP method @@ -147,7 +140,7 @@ public function testLoggingParameter($parameter, $log, $parameters) $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('http://localhost/'); - $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext(), $logger); + $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext(), $logger); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); } @@ -185,7 +178,7 @@ public function testNoRoutingConfigurationResponse() $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('matchRequest') ->willThrowException(new NoConfigurationException()) ; @@ -196,6 +189,11 @@ public function testNoRoutingConfigurationResponse() $kernel = new HttpKernel($dispatcher, new ControllerResolver(), $requestStack, new ArgumentResolver()); $request = Request::create('http://localhost/'); + + $response = $kernel->handle($request); + $this->assertSame(404, $response->getStatusCode()); + $this->assertStringContainsString('Welcome', $response->getContent()); + $response = $kernel->handle($request); $this->assertSame(404, $response->getStatusCode()); $this->assertStringContainsString('Welcome', $response->getContent()); @@ -210,7 +208,7 @@ public function testRequestWithBadHost() $requestMatcher = $this->createMock(RequestMatcherInterface::class); - $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext()); + $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); } @@ -237,7 +235,7 @@ public function testResourceNotFoundException() $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - $listener = new RouterListener($urlMatcher, $this->requestStack); + $listener = new RouterListener($urlMatcher, new RequestStack()); $listener->onKernelRequest($event); } @@ -263,7 +261,111 @@ public function testMethodNotAllowedException() $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - $listener = new RouterListener($urlMatcher, $this->requestStack); + $listener = new RouterListener($urlMatcher, new RequestStack()); + $listener->onKernelRequest($event); + } + + /** + * @dataProvider provideRouteMapping + */ + public function testRouteMapping(array $expected, array $parameters) + { + $kernel = $this->createMock(HttpKernelInterface::class); + $request = Request::create('http://localhost/'); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $requestMatcher = $this->createMock(RequestMatcherInterface::class); + $requestMatcher->expects($this->any()) + ->method('matchRequest') + ->with($this->isInstanceOf(Request::class)) + ->willReturn($parameters); + + $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); + + $expected['_route_mapping'] = $parameters['_route_mapping']; + unset($parameters['_route_mapping']); + $expected['_route_params'] = $parameters; + + $this->assertEquals($expected, $request->attributes->all()); + } + + public static function provideRouteMapping(): iterable + { + yield [ + [ + 'conference' => 'vienna-2024', + ], + [ + 'slug' => 'vienna-2024', + '_route_mapping' => [ + 'slug' => 'conference', + ], + ], + ]; + + yield [ + [ + 'article' => [ + 'id' => 'abc123', + 'date' => '2024-04-24', + 'slug' => 'symfony-rocks', + ], + ], + [ + 'id' => 'abc123', + 'date' => '2024-04-24', + 'slug' => 'symfony-rocks', + '_route_mapping' => [ + 'id' => 'article', + 'date' => 'article', + 'slug' => 'article', + ], + ], + ]; + + 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', + ], + ], + ], + ]; } } diff --git a/Tests/EventListener/SessionListenerTest.php b/Tests/EventListener/SessionListenerTest.php index 3064fca51f..fda957bb49 100644 --- a/Tests/EventListener/SessionListenerTest.php +++ b/Tests/EventListener/SessionListenerTest.php @@ -104,13 +104,13 @@ public static function provideSessionOptions(): \Generator yield 'set_cookiesecure_auto_by_symfony_false_by_php' => [ 'phpSessionOptions' => ['secure' => false], - 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => 'auto', 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => false, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_cookiesecure_auto_by_symfony_true_by_php' => [ 'phpSessionOptions' => ['secure' => true], - 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => 'auto', 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; @@ -336,7 +336,7 @@ public function testSessionCookieSetWhenOtherNativeVariablesSet() public function testOnlyTriggeredOnMainRequest() { - $listener = new class() extends AbstractSessionListener { + $listener = new class extends AbstractSessionListener { protected function getSession(): ?SessionInterface { return null; @@ -358,14 +358,12 @@ public function testSessionIsSet() $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); - $requestStack = $this->createMock(RequestStack::class); - $sessionStorage = $this->createMock(NativeSessionStorage::class); $sessionStorage->expects($this->never())->method('setOptions')->with(['cookie_secure' => true]); $container = new Container(); $container->set('session_factory', $sessionFactory); - $container->set('request_stack', $requestStack); + $container->set('request_stack', new RequestStack()); $request = new Request(); $listener = new SessionListener($container); @@ -898,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()); } @@ -919,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/Exception/HttpExceptionTest.php b/Tests/Exception/HttpExceptionTest.php index 781cb85eb6..b31bd75a79 100644 --- a/Tests/Exception/HttpExceptionTest.php +++ b/Tests/Exception/HttpExceptionTest.php @@ -63,6 +63,38 @@ public function testThrowableIsAllowedForPrevious() $this->assertSame($previous, $exception->getPrevious()); } + /** + * @dataProvider provideStatusCode + */ + public function testFromStatusCode(int $statusCode) + { + $exception = HttpException::fromStatusCode($statusCode); + $this->assertInstanceOf(HttpException::class, $exception); + $this->assertSame($statusCode, $exception->getStatusCode()); + } + + public static function provideStatusCode() + { + return [ + [400], + [401], + [403], + [404], + [406], + [409], + [410], + [411], + [412], + [418], + [423], + [415], + [422], + [428], + [429], + [503], + ]; + } + protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new HttpException(200, $message, $previous, $headers, $code); diff --git a/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php b/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php index 4fba6260f9..ef75bdfa08 100644 --- a/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php +++ b/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php @@ -31,7 +31,7 @@ public function configure(DefinitionConfigurator $definition): void public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { - $container->extension('loaded', ['bar' => 'baz']); + $builder->prependExtensionConfig('loaded', ['bar' => 'baz']); } public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void diff --git a/Tests/Fixtures/Controller/ArgumentResolver/UploadedFile/file-big.txt b/Tests/Fixtures/Controller/ArgumentResolver/UploadedFile/file-big.txt new file mode 100644 index 0000000000..450222562e --- /dev/null +++ b/Tests/Fixtures/Controller/ArgumentResolver/UploadedFile/file-big.txt @@ -0,0 +1 @@ +I'm not big, but I'm big enough to carry more than 50 bytes inside me. diff --git a/Tests/Fixtures/Controller/ArgumentResolver/UploadedFile/file-small.txt b/Tests/Fixtures/Controller/ArgumentResolver/UploadedFile/file-small.txt new file mode 100644 index 0000000000..fa7c2c3885 --- /dev/null +++ b/Tests/Fixtures/Controller/ArgumentResolver/UploadedFile/file-small.txt @@ -0,0 +1 @@ +I'm a file with less than 50 bytes. diff --git a/Tests/Fixtures/KernelForTest.php b/Tests/Fixtures/KernelForTest.php deleted file mode 100644 index 9146e46910..0000000000 --- a/Tests/Fixtures/KernelForTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\Tests\Fixtures; - -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpKernel\Kernel; - -class KernelForTest extends Kernel -{ - public function __construct(string $environment, bool $debug, private readonly bool $fakeContainer = true) - { - parent::__construct($environment, $debug); - } - - public function getBundleMap(): array - { - return []; - } - - public function registerBundles(): iterable - { - return []; - } - - public function registerContainerConfiguration(LoaderInterface $loader): void - { - } - - public function isBooted(): bool - { - return $this->booted; - } - - public function getProjectDir(): string - { - return __DIR__; - } - - protected function initializeContainer(): void - { - if ($this->fakeContainer) { - $this->container = new ContainerBuilder(); - } else { - parent::initializeContainer(); - } - } -} 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; diff --git a/Tests/Fragment/FragmentHandlerTest.php b/Tests/Fragment/FragmentHandlerTest.php index c9e22633b7..7266eb3c59 100644 --- a/Tests/Fragment/FragmentHandlerTest.php +++ b/Tests/Fragment/FragmentHandlerTest.php @@ -25,12 +25,8 @@ class FragmentHandlerTest extends TestCase protected function setUp(): void { - $this->requestStack = $this->createMock(RequestStack::class); - $this->requestStack - ->expects($this->any()) - ->method('getCurrentRequest') - ->willReturn(Request::create('/')) - ; + $this->requestStack = new RequestStack(); + $this->requestStack->push(Request::create('/')); } public function testRenderWhenRendererDoesNotExist() 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 a72c08b872..9f03f6d8e8 100644 --- a/Tests/HttpCache/HttpCacheTest.php +++ b/Tests/HttpCache/HttpCacheTest.php @@ -11,13 +11,13 @@ namespace Symfony\Component\HttpKernel\Tests\HttpCache; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; +use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpCache\StoreInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; @@ -27,8 +27,6 @@ */ class HttpCacheTest extends HttpCacheTestCase { - use ExpectDeprecationTrait; - public function testTerminateDelegatesTerminationOnlyForTerminableInterface() { $storeMock = $this->getMockBuilder(StoreInterface::class) @@ -37,7 +35,7 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface() // does not implement TerminableInterface $kernel = new TestKernel(); - $httpCache = new HttpCache($kernel, $storeMock, null, ['terminate_on_cache_hit' => false]); + $httpCache = new HttpCache($kernel, $storeMock); $httpCache->terminate(Request::create('/'), new Response()); $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface'); @@ -51,7 +49,7 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface() $kernelMock->expects($this->once()) ->method('terminate'); - $kernel = new HttpCache($kernelMock, $storeMock, null, ['terminate_on_cache_hit' => false]); + $kernel = new HttpCache($kernelMock, $storeMock); $kernel->terminate(Request::create('/'), new Response()); } @@ -101,58 +99,6 @@ public function testDoesNotCallTerminateOnFreshResponse() $this->assertCount(1, $terminateEvents); } - /** - * @group legacy - */ - public function testDoesCallTerminateOnFreshResponseIfConfigured() - { - $this->expectDeprecation('Since symfony/http-kernel 6.2: Setting "terminate_on_cache_hit" to "true" is deprecated and will be changed to "false" in Symfony 7.0.'); - - $terminateEvents = []; - - $eventDispatcher = $this->createMock(EventDispatcher::class); - $eventDispatcher - ->expects($this->any()) - ->method('dispatch') - ->with($this->callback(function ($event) use (&$terminateEvents) { - if ($event instanceof TerminateEvent) { - $terminateEvents[] = $event; - } - - return true; - })); - - $this->setNextResponse( - 200, - [ - 'ETag' => '1234', - 'Cache-Control' => 'public, s-maxage=60', - ], - 'Hello World', - null, - $eventDispatcher - ); - $this->cacheConfig['terminate_on_cache_hit'] = true; - - $this->request('GET', '/'); - $this->assertHttpKernelIsCalled(); - $this->assertEquals(200, $this->response->getStatusCode()); - $this->assertTraceContains('miss'); - $this->assertTraceContains('store'); - $this->cache->terminate($this->request, $this->response); - - sleep(2); - - $this->request('GET', '/'); - $this->assertHttpKernelIsNotCalled(); - $this->assertEquals(200, $this->response->getStatusCode()); - $this->assertTraceContains('fresh'); - $this->assertEquals(2, $this->response->headers->get('Age')); - $this->cache->terminate($this->request, $this->response); - - $this->assertCount(2, $terminateEvents); - } - public function testPassesOnNonGetHeadRequests() { $this->setNextResponse(200); @@ -250,7 +196,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'); } @@ -264,7 +210,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'); } @@ -330,7 +276,7 @@ public function testValidatesPrivateResponsesCachedOnTheClient() if ($request->cookies->has('authenticated')) { $response->headers->set('Cache-Control', 'private, no-store'); $response->setETag('"private tag"'); - if (\in_array('"private tag"', $etags)) { + if (\in_array('"private tag"', $etags, true)) { $response->setStatusCode(304); } else { $response->setStatusCode(200); @@ -340,7 +286,7 @@ public function testValidatesPrivateResponsesCachedOnTheClient() } else { $response->headers->set('Cache-Control', 'public'); $response->setETag('"public tag"'); - if (\in_array('"public tag"', $etags)) { + if (\in_array('"public tag"', $etags, true)) { $response->setStatusCode(304); } else { $response->setStatusCode(200); @@ -717,6 +663,7 @@ public function testDegradationWhenCacheLocked() */ sleep(10); + $this->store = $this->createStore(); // create another store instance that does not hold the current lock $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); @@ -735,6 +682,64 @@ public function testDegradationWhenCacheLocked() $this->assertEquals('Old response', $this->response->getContent()); } + public function testHitBackendOnlyOnceWhenCacheWasLocked() + { + // Disable stale-while-revalidate, it circumvents waiting for the lock + $this->cacheConfig['stale_while_revalidate'] = 0; + + $this->setNextResponses([ + [ + 'status' => 200, + 'body' => 'initial response', + 'headers' => [ + 'Cache-Control' => 'public, no-cache', + 'Last-Modified' => 'some while ago', + ], + ], + [ + 'status' => 304, + 'body' => '', + 'headers' => [ + 'Cache-Control' => 'public, no-cache', + 'Last-Modified' => 'some while ago', + ], + ], + [ + 'status' => 500, + 'body' => 'The backend should not be called twice during revalidation', + 'headers' => [], + ], + ]); + + $this->request('GET', '/'); // warm the cache + + // Use a store that simulates a cache entry being locked upon first attempt + $this->store = new class(sys_get_temp_dir().'/http_cache') extends Store { + private bool $hasLock = false; + + public function lock(Request $request): bool + { + $hasLock = $this->hasLock; + $this->hasLock = true; + + return $hasLock; + } + + public function isLocked(Request $request): bool + { + return false; + } + }; + + $this->request('GET', '/'); // hit the cache with simulated lock/concurrency block + + $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertEquals('initial response', $this->response->getContent()); + + $traces = $this->cache->getTraces(); + $this->assertSame(['stale', 'valid', 'store'], current($traces)); + } + public function testHitsCachedResponseWithSMaxAgeDirective() { $time = \DateTimeImmutable::createFromFormat('U', time() - 5); @@ -1336,7 +1341,7 @@ public function testEsiCacheSendsTheLowestTtlForHeadRequests() $this->request('HEAD', '/', [], [], true); - $this->assertEmpty($this->response->getContent()); + $this->assertSame('', $this->response->getContent()); $this->assertEquals(100, $this->response->getTtl()); } @@ -1358,7 +1363,7 @@ public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFails 'headers' => [ 'Cache-Control' => 's-maxage=10', // stays fresh 'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000', - ] + ], ], ]); @@ -1415,7 +1420,7 @@ public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFails 'headers' => [ 'Cache-Control' => 's-maxage=0', // goes stale immediately 'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000', - ] + ], ], ]); @@ -1480,7 +1485,7 @@ public function testEsiCacheIncludesEmbeddedResponseContentWhenMainAndEmbeddedRe 'headers' => [ 'Cache-Control' => 's-maxage=10', 'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000', - ] + ], ], ]); @@ -1565,7 +1570,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')); @@ -1623,7 +1628,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')); } @@ -1747,7 +1752,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/HttpCacheTestCase.php b/Tests/HttpCache/HttpCacheTestCase.php index 26a29f16b2..b05d913666 100644 --- a/Tests/HttpCache/HttpCacheTestCase.php +++ b/Tests/HttpCache/HttpCacheTestCase.php @@ -30,7 +30,7 @@ abstract class HttpCacheTestCase extends TestCase protected $responses; protected $catch; protected $esi; - protected Store $store; + protected ?Store $store = null; protected function setUp(): void { @@ -115,7 +115,9 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi = $this->kernel->reset(); - $this->store = new Store(sys_get_temp_dir().'/http_cache'); + if (!$this->store) { + $this->store = $this->createStore(); + } if (!isset($this->cacheConfig['debug'])) { $this->cacheConfig['debug'] = true; @@ -183,4 +185,9 @@ public static function clearDirectory($directory) closedir($fp); } + + protected function createStore(): Store + { + return new Store(sys_get_temp_dir().'/http_cache'); + } } diff --git a/Tests/HttpCache/ResponseCacheStrategyTest.php b/Tests/HttpCache/ResponseCacheStrategyTest.php index ef3f495011..a4650643cb 100644 --- a/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -400,14 +400,14 @@ public function testCacheControlMerging(array $expects, array $main, array $surr } elseif ('age' === $key) { $this->assertSame($value, $response->getAge()); } elseif (true === $value) { - $this->assertTrue($response->headers->hasCacheControlDirective($key), sprintf('Cache-Control header must have "%s" flag', $key)); + $this->assertTrue($response->headers->hasCacheControlDirective($key), \sprintf('Cache-Control header must have "%s" flag', $key)); } elseif (false === $value) { $this->assertFalse( $response->headers->hasCacheControlDirective($key), - sprintf('Cache-Control header must NOT have "%s" flag', $key) + \sprintf('Cache-Control header must NOT have "%s" flag', $key) ); } else { - $this->assertSame($value, $response->headers->getCacheControlDirective($key), sprintf('Cache-Control flag "%s" should be "%s"', $key, $value)); + $this->assertSame($value, $response->headers->getCacheControlDirective($key), \sprintf('Cache-Control flag "%s" should be "%s"', $key, $value)); } } } diff --git a/Tests/HttpCache/StoreTest.php b/Tests/HttpCache/StoreTest.php index 1942e8d337..a24aa95c87 100644 --- a/Tests/HttpCache/StoreTest.php +++ b/Tests/HttpCache/StoreTest.php @@ -15,7 +15,6 @@ use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; class StoreTest extends TestCase @@ -41,7 +40,7 @@ protected function tearDown(): void public function testReadsAnEmptyArrayWithReadWhenNothingCachedAtKey() { - $this->assertEmpty($this->getStoreMetadata('/nothing')); + $this->assertSame([], $this->getStoreMetadata('/nothing')); } public function testUnlockFileThatDoesExist() @@ -66,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]); @@ -292,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() @@ -307,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/HttpKernelTest.php b/Tests/HttpKernelTest.php index 585b3383de..58e6ab5107 100644 --- a/Tests/HttpKernelTest.php +++ b/Tests/HttpKernelTest.php @@ -52,7 +52,7 @@ public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCat $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \RuntimeException(), $requestStack); try { - $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); } catch (\Throwable $exception) { } @@ -65,7 +65,7 @@ public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCat $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \RuntimeException(), $requestStack); try { - $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, false); + $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); } catch (\Throwable $exception) { } @@ -78,7 +78,7 @@ public function testRequestStackIsNotBrokenWhenControllerThrowsAnThrowable() $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \Error(), $requestStack); try { - $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); } catch (\Throwable $exception) { } diff --git a/Tests/KernelTest.php b/Tests/KernelTest.php index 94c498a0f2..f650c33ee0 100644 --- a/Tests/KernelTest.php +++ b/Tests/KernelTest.php @@ -12,15 +12,11 @@ namespace Symfony\Component\HttpKernel\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; @@ -33,15 +29,11 @@ use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest; -use Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTestWithLoadClassCache; use Symfony\Component\HttpKernel\Tests\Fixtures\KernelWithoutBundles; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; class KernelTest extends TestCase { - use ExpectDeprecationTrait; - protected function tearDown(): void { try { @@ -65,7 +57,7 @@ public function testConstructor() public function testEmptyEnv() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage(sprintf('Invalid environment provided to "%s": the environment cannot be empty.', KernelForTest::class)); + $this->expectExceptionMessage(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', KernelForTest::class)); new KernelForTest('', false); } @@ -248,117 +240,12 @@ public function testHandleBootsTheKernel() $kernel->handle($request, $type, $catch); } - /** - * @dataProvider getStripCommentsCodes - * - * @group legacy - */ - public function testStripComments(string $source, string $expected) - { - $this->expectDeprecation('Since symfony/http-kernel 6.4: Method "Symfony\Component\HttpKernel\Kernel::stripComments()" is deprecated without replacement.'); - - $output = Kernel::stripComments($source); - - // Heredocs are preserved, making the output mixing Unix and Windows line - // endings, switching to "\n" everywhere on Windows to avoid failure. - if ('\\' === \DIRECTORY_SEPARATOR) { - $expected = str_replace("\r\n", "\n", $expected); - $output = str_replace("\r\n", "\n", $output); - } - - $this->assertEquals($expected, $output); - } - - public static function getStripCommentsCodes(): array - { - return [ - ['assertEquals($expected, serialize($kernel)); } @@ -523,7 +410,7 @@ public function testKernelReset() public function testKernelExtension() { - $kernel = new class() extends CustomProjectDirKernel implements ExtensionInterface { + $kernel = new class extends CustomProjectDirKernel implements ExtensionInterface { public function load(array $configs, ContainerBuilder $container): void { $container->setParameter('test.extension-registered', true); @@ -634,43 +521,23 @@ public function getContainerClass(): string $this->assertMatchesRegularExpression('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*TestDebugContainer$/', $kernel->getContainerClass()); } - /** - * @group legacy - */ - public function testKernelWithParameterDeprecation() + public function testTrustedParameters() { - $kernel = new class('test', true) extends Kernel { - public function __construct(string $env, bool $debug) - { - $this->container = new ContainerBuilder(new ParameterBag(['container.dumper.inline_factories' => true, 'container.dumper.inline_class_loader' => true])); - parent::__construct($env, $debug); - } - - public function registerBundles(): iterable - { - return []; - } - - public function registerContainerConfiguration(LoaderInterface $loader): void - { - } - - public function boot(): void - { - $this->container->compile(); - parent::dumpContainer(new ConfigCache(tempnam(sys_get_temp_dir(), 'symfony-kernel-deprecated-parameter'), true), $this->container, Container::class, $this->getContainerBaseClass()); - } - - public function getContainerClass(): string - { - return parent::getContainerClass(); - } - }; - - $this->expectDeprecation('Since symfony/http-kernel 6.3: Parameter "container.dumper.inline_factories" is deprecated, use ".container.dumper.inline_factories" instead.'); - $this->expectDeprecation('Since symfony/http-kernel 6.3: Parameter "container.dumper.inline_class_loader" is deprecated, use ".container.dumper.inline_class_loader" instead.'); - + $kernel = new CustomProjectDirKernel(function (ContainerBuilder $container) { + $container->setParameter('kernel.trusted_hosts', '^a{2,3}.com$, ^b{2,}.com$'); + $container->setParameter('kernel.trusted_proxies', 'a,b'); + $container->setParameter('kernel.trusted_headers', 'x-forwarded-for'); + }); $kernel->boot(); + + try { + $this->assertSame(['{^a{2,3}.com$}i', '{^b{2,}.com$}i'], Request::getTrustedHosts()); + $this->assertSame(['a', 'b'], Request::getTrustedProxies()); + $this->assertSame(Request::HEADER_X_FORWARDED_FOR, Request::getTrustedHeaderSet()); + } finally { + Request::setTrustedHosts([]); + Request::setTrustedProxies([], 0); + } } /** @@ -811,3 +678,51 @@ public function process(ContainerBuilder $container): void $container->setParameter('test.processed', true); } } + +class KernelForTest extends Kernel +{ + public function __construct(string $environment, bool $debug, private readonly bool $fakeContainer = true) + { + parent::__construct($environment, $debug); + } + + public function getBundleMap(): array + { + return []; + } + + public function registerBundles(): iterable + { + return []; + } + + public function registerContainerConfiguration(LoaderInterface $loader): void + { + } + + public function isBooted(): bool + { + return $this->booted; + } + + public function getProjectDir(): string + { + return __DIR__; + } + + protected function initializeContainer(): void + { + if ($this->fakeContainer) { + $this->container = new ContainerBuilder(); + } else { + parent::initializeContainer(); + } + } +} + +class KernelForTestWithLoadClassCache extends KernelForTest +{ + public function doLoadClassCache(): void + { + } +} diff --git a/Tests/Log/LoggerTest.php b/Tests/Log/LoggerTest.php index f6c943e88b..b70733d3bf 100644 --- a/Tests/Log/LoggerTest.php +++ b/Tests/Log/LoggerTest.php @@ -129,11 +129,7 @@ public function testContextReplacement() public function testObjectCastToString() { - if (method_exists($this, 'createPartialMock')) { - $dummy = $this->createPartialMock(DummyTest::class, ['__toString']); - } else { - $dummy = $this->createPartialMock(DummyTest::class, ['__toString']); - } + $dummy = $this->createPartialMock(DummyTest::class, ['__toString']); $dummy->expects($this->atLeastOnce()) ->method('__toString') ->willReturn('DUMMY'); diff --git a/Tests/Logger.php b/Tests/Logger.php index 22ef90d423..3620510e40 100644 --- a/Tests/Logger.php +++ b/Tests/Logger.php @@ -11,9 +11,9 @@ namespace Symfony\Component\HttpKernel\Tests; -use Psr\Log\LoggerInterface; +use Psr\Log\AbstractLogger; -class Logger implements LoggerInterface +class Logger extends AbstractLogger { protected array $logs; @@ -22,9 +22,9 @@ public function __construct() $this->clear(); } - public function getLogs($level = false): array + public function getLogsForLevel(string $level): array { - return false === $level ? $this->logs : $this->logs[$level]; + return $this->logs[$level]; } public function clear(): void @@ -45,44 +45,4 @@ public function log($level, $message, array $context = []): void { $this->logs[$level][] = $message; } - - public function emergency($message, array $context = []): void - { - $this->log('emergency', $message, $context); - } - - public function alert($message, array $context = []): void - { - $this->log('alert', $message, $context); - } - - public function critical($message, array $context = []): void - { - $this->log('critical', $message, $context); - } - - public function error($message, array $context = []): void - { - $this->log('error', $message, $context); - } - - public function warning($message, array $context = []): void - { - $this->log('warning', $message, $context); - } - - public function notice($message, array $context = []): void - { - $this->log('notice', $message, $context); - } - - public function info($message, array $context = []): void - { - $this->log('info', $message, $context); - } - - public function debug($message, array $context = []): void - { - $this->log('debug', $message, $context); - } } 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'); } diff --git a/Tests/UriSignerTest.php b/Tests/UriSignerTest.php deleted file mode 100644 index c701a894e2..0000000000 --- a/Tests/UriSignerTest.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\UriSigner; - -/** - * @group legacy - */ -class UriSignerTest extends TestCase -{ - public function testSign() - { - $signer = new UriSigner('foobar'); - - $this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo')); - $this->assertStringContainsString('?_hash=', $signer->sign('http://example.com/foo?foo=bar')); - $this->assertStringContainsString('&foo=', $signer->sign('http://example.com/foo?foo=bar')); - } - - public function testCheck() - { - $signer = new UriSigner('foobar'); - - $this->assertFalse($signer->check('http://example.com/foo?_hash=foo')); - $this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo')); - $this->assertFalse($signer->check('http://example.com/foo?foo=bar&_hash=foo&bar=foo')); - - $this->assertTrue($signer->check($signer->sign('http://example.com/foo'))); - $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar'))); - $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&0=integer'))); - - $this->assertSame($signer->sign('http://example.com/foo?foo=bar&bar=foo'), $signer->sign('http://example.com/foo?bar=foo&foo=bar')); - } - - public function testCheckWithDifferentArgSeparator() - { - $initialSeparatorOutput = ini_set('arg_separator.output', '&'); - - try { - $signer = new UriSigner('foobar'); - - $this->assertSame( - 'http://example.com/foo?_hash=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D&baz=bay&foo=bar', - $signer->sign('http://example.com/foo?foo=bar&baz=bay') - ); - $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay'))); - } finally { - ini_set('arg_separator.output', $initialSeparatorOutput); - } - } - - public function testCheckWithRequest() - { - $signer = new UriSigner('foobar'); - - $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo')))); - $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar')))); - $this->assertTrue($signer->checkRequest(Request::create($signer->sign('http://example.com/foo?foo=bar&0=integer')))); - } - - public function testCheckWithDifferentParameter() - { - $signer = new UriSigner('foobar', 'qux'); - - $this->assertSame( - 'http://example.com/foo?baz=bay&foo=bar&qux=rIOcC%2FF3DoEGo%2FvnESjSp7uU9zA9S%2F%2BOLhxgMexoPUM%3D', - $signer->sign('http://example.com/foo?foo=bar&baz=bay') - ); - $this->assertTrue($signer->check($signer->sign('http://example.com/foo?foo=bar&baz=bay'))); - } - - public function testSignerWorksWithFragments() - { - $signer = new UriSigner('foobar'); - - $this->assertSame( - 'http://example.com/foo?_hash=EhpAUyEobiM3QTrKxoLOtQq5IsWyWedoXDPqIjzNj5o%3D&bar=foo&foo=bar#foobar', - $signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar') - ); - $this->assertTrue($signer->check($signer->sign('http://example.com/foo?bar=foo&foo=bar#foobar'))); - } -} diff --git a/UriSigner.php b/UriSigner.php deleted file mode 100644 index fb5383cdec..0000000000 --- a/UriSigner.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\HttpKernel; - -use Symfony\Component\HttpFoundation\UriSigner as HttpFoundationUriSigner; - -trigger_deprecation('symfony/http-kernel', '6.4', 'The "%s" class is deprecated, use "%s" instead.', UriSigner::class, HttpFoundationUriSigner::class); - -class_exists(HttpFoundationUriSigner::class); - -if (false) { - /** - * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link HttpFoundationUriSigner} instead - */ - class UriSigner extends HttpFoundationUriSigner - { - } -} diff --git a/composer.json b/composer.json index 1c70224aa6..bb9f4ba617 100644 --- a/composer.json +++ b/composer.json @@ -16,60 +16,60 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", "symfony/polyfill-ctype": "^1.8", "psr/log": "^1|^2|^3" }, "require-dev": { - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/clock": "^6.2|^7.0", - "symfony/config": "^6.1|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/property-access": "^5.4.5|^6.0.5|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4.4|^7.0.4", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^5.4|^6.4|^7.0", - "symfony/var-exporter": "^6.2|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", "psr/cache": "^1.0|^2.0|^3.0", - "twig/twig": "^2.13|^3.0.4" + "twig/twig": "^3.12" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.4", - "symfony/config": "<6.1", - "symfony/console": "<5.4", - "symfony/form": "<5.4", + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/form": "<6.4", "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<5.4", - "symfony/http-client": "<5.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<5.4", - "symfony/messenger": "<5.4", - "symfony/translation": "<5.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<5.4", + "symfony/twig-bridge": "<6.4", "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.3", - "twig/twig": "<2.13" + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpKernel\\": "" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy