diff --git a/Attribute/ArgumentInterface.php b/Attribute/ArgumentInterface.php new file mode 100644 index 0000000000..78769f1ac0 --- /dev/null +++ b/Attribute/ArgumentInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +trigger_deprecation('symfony/http-kernel', '5.3', 'The "%s" interface is deprecated.', ArgumentInterface::class); + +/** + * Marker interface for controller argument attributes. + * + * @deprecated since Symfony 5.3 + */ +interface ArgumentInterface +{ +} diff --git a/Attribute/AsController.php b/Attribute/AsController.php new file mode 100644 index 0000000000..ef37104513 --- /dev/null +++ b/Attribute/AsController.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +/** + * Service tag to autoconfigure controllers. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsController +{ + public function __construct() + { + } +} diff --git a/Bundle/Bundle.php b/Bundle/Bundle.php index 2e65f67c9d..54a1d10b90 100644 --- a/Bundle/Bundle.php +++ b/Bundle/Bundle.php @@ -58,7 +58,7 @@ public function build(ContainerBuilder $container) /** * Returns the bundle's container extension. * - * @return ExtensionInterface|null The container extension + * @return ExtensionInterface|null * * @throws \LogicException */ diff --git a/Bundle/BundleInterface.php b/Bundle/BundleInterface.php index 88a95d8332..fdc13e0c87 100644 --- a/Bundle/BundleInterface.php +++ b/Bundle/BundleInterface.php @@ -42,21 +42,21 @@ public function build(ContainerBuilder $container); /** * Returns the container extension that should be implicitly loaded. * - * @return ExtensionInterface|null The default extension or null if there is none + * @return ExtensionInterface|null */ public function getContainerExtension(); /** * Returns the bundle name (the class short name). * - * @return string The Bundle name + * @return string */ public function getName(); /** * Gets the Bundle namespace. * - * @return string The Bundle namespace + * @return string */ public function getNamespace(); @@ -65,7 +65,7 @@ public function getNamespace(); * * The path should always be returned as a Unix path (with /). * - * @return string The Bundle absolute path + * @return string */ public function getPath(); } diff --git a/CHANGELOG.md b/CHANGELOG.md index b74c4b8757..d0dc2076c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,42 @@ CHANGELOG ========= +5.4 +--- + + * Add the ability to enable the profiler using a request query parameter, body parameter or attribute + * Deprecate `AbstractTestSessionListener` and `TestSessionListener`, use `AbstractSessionListener` and `SessionListener` instead + * Deprecate the `fileLinkFormat` parameter of `DebugHandlersListener` + * Add support for configuring log level, and status code by exception class + * Allow ignoring "kernel.reset" methods that don't exist with "on_invalid" attribute + +5.3 +--- + + * Deprecate `ArgumentInterface` + * Add `ArgumentMetadata::getAttributes()` + * Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead + * Mark the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal + * Deprecate returning a `ContainerBuilder` from `KernelInterface::registerContainerConfiguration()` + * Deprecate `HttpKernelInterface::MASTER_REQUEST` and add `HttpKernelInterface::MAIN_REQUEST` as replacement + * Deprecate `KernelEvent::isMasterRequest()` and add `isMainRequest()` as replacement + * Add `#[AsController]` attribute for declaring standalone controllers on PHP 8 + * Add `FragmentUriGeneratorInterface` and `FragmentUriGenerator` to generate the URI of a fragment + +5.2.0 +----- + + * added session usage + * made the public `http_cache` service handle requests when available + * allowed enabling trusted hosts and proxies using new `kernel.trusted_hosts`, + `kernel.trusted_proxies` and `kernel.trusted_headers` parameters + * content of request parameter `_password` is now also hidden + in the request profiler raw content section + * Allowed adding attributes on controller arguments that will be passed to argument resolvers. + * kernels implementing the `ExtensionInterface` will now be auto-registered to the container + * added parameter `kernel.runtime_environment`, defined as `%env(default:kernel.environment:APP_RUNTIME_ENV)%` + * do not set a default `Accept` HTTP header when using `HttpKernelBrowser` + 5.1.0 ----- diff --git a/CacheClearer/ChainCacheClearer.php b/CacheClearer/ChainCacheClearer.php index 95d41a8db6..a875d899d0 100644 --- a/CacheClearer/ChainCacheClearer.php +++ b/CacheClearer/ChainCacheClearer.php @@ -22,6 +22,9 @@ class ChainCacheClearer implements CacheClearerInterface { private $clearers; + /** + * @param iterable $clearers + */ public function __construct(iterable $clearers = []) { $this->clearers = $clearers; diff --git a/CacheClearer/Psr6CacheClearer.php b/CacheClearer/Psr6CacheClearer.php index d0e4cc91b7..a074060e44 100644 --- a/CacheClearer/Psr6CacheClearer.php +++ b/CacheClearer/Psr6CacheClearer.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\CacheClearer; +use Psr\Cache\CacheItemPoolInterface; + /** * @author Nicolas Grekas */ @@ -18,16 +20,27 @@ class Psr6CacheClearer implements CacheClearerInterface { private $pools = []; + /** + * @param array $pools + */ public function __construct(array $pools = []) { $this->pools = $pools; } + /** + * @return bool + */ public function hasPool(string $name) { return isset($this->pools[$name]); } + /** + * @return CacheItemPoolInterface + * + * @throws \InvalidArgumentException If the cache pool with the given name does not exist + */ public function getPool(string $name) { if (!$this->hasPool($name)) { @@ -37,6 +50,11 @@ public function getPool(string $name) return $this->pools[$name]; } + /** + * @return bool + * + * @throws \InvalidArgumentException If the cache pool with the given name does not exist + */ public function clearPool(string $name) { if (!isset($this->pools[$name])) { diff --git a/CacheWarmer/CacheWarmerAggregate.php b/CacheWarmer/CacheWarmerAggregate.php index 30e82449b4..67f9ed50b4 100644 --- a/CacheWarmer/CacheWarmerAggregate.php +++ b/CacheWarmer/CacheWarmerAggregate.php @@ -26,6 +26,9 @@ class CacheWarmerAggregate implements CacheWarmerInterface private $optionalsEnabled = false; private $onlyOptionalsEnabled = false; + /** + * @param iterable $warmers + */ public function __construct(iterable $warmers = [], bool $debug = false, string $deprecationLogsFilepath = null) { $this->warmers = $warmers; @@ -44,11 +47,9 @@ public function enableOnlyOptionalWarmers() } /** - * Warms up the cache. - * - * @return string[] A list of classes or files to preload on PHP 7.4+ + * {@inheritdoc} */ - public function warmUp(string $cacheDir) + public function warmUp(string $cacheDir): array { if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) { $collectedLogs = []; @@ -101,9 +102,11 @@ public function warmUp(string $cacheDir) if ($collectDeprecations) { restore_error_handler(); - if (file_exists($this->deprecationLogsFilepath)) { + if (is_file($this->deprecationLogsFilepath)) { $previousLogs = unserialize(file_get_contents($this->deprecationLogsFilepath)); - $collectedLogs = array_merge($previousLogs, $collectedLogs); + if (\is_array($previousLogs)) { + $collectedLogs = array_merge($previousLogs, $collectedLogs); + } } file_put_contents($this->deprecationLogsFilepath, serialize(array_values($collectedLogs))); @@ -114,9 +117,7 @@ public function warmUp(string $cacheDir) } /** - * Checks whether this warmer is optional or not. - * - * @return bool always false + * {@inheritdoc} */ public function isOptional(): bool { diff --git a/CacheWarmer/CacheWarmerInterface.php b/CacheWarmer/CacheWarmerInterface.php index 8fece5e954..1f1740b7e2 100644 --- a/CacheWarmer/CacheWarmerInterface.php +++ b/CacheWarmer/CacheWarmerInterface.php @@ -26,7 +26,7 @@ interface CacheWarmerInterface extends WarmableInterface * A warmer should return true if the cache can be * generated incrementally and on-demand. * - * @return bool true if the warmer is optional, false otherwise + * @return bool */ public function isOptional(); } diff --git a/Controller/ArgumentResolver.php b/Controller/ArgumentResolver.php index 4285ba7631..a54140b7e5 100644 --- a/Controller/ArgumentResolver.php +++ b/Controller/ArgumentResolver.php @@ -28,15 +28,14 @@ final class ArgumentResolver implements ArgumentResolverInterface { private $argumentMetadataFactory; + private $argumentValueResolvers; /** - * @var iterable|ArgumentValueResolverInterface[] + * @param iterable $argumentValueResolvers */ - private $argumentValueResolvers; - public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = []) { - $this->argumentMetadataFactory = $argumentMetadataFactory ?: new ArgumentMetadataFactory(); + $this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory(); $this->argumentValueResolvers = $argumentValueResolvers ?: self::getDefaultArgumentValueResolvers(); } @@ -83,6 +82,9 @@ public function getArguments(Request $request, callable $controller): array return $arguments; } + /** + * @return iterable + */ public static function getDefaultArgumentValueResolvers(): iterable { return [ diff --git a/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php b/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php index d4971cc1a5..48ea6e742d 100644 --- a/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php +++ b/Controller/ArgumentResolver/NotTaggedControllerValueResolver.php @@ -69,8 +69,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable } if (!$this->container->has($controller)) { - $i = strrpos($controller, ':'); - $controller = substr($controller, 0, $i).strtolower(substr($controller, $i)); + $controller = (false !== $i = strrpos($controller, ':')) + ? substr($controller, 0, $i).strtolower(substr($controller, $i)) + : $controller.'::__invoke'; } $what = sprintf('argument $%s of "%s()"', $argument->getName(), $controller); diff --git a/Controller/ArgumentResolverInterface.php b/Controller/ArgumentResolverInterface.php index 2c32492cf4..30e4783e89 100644 --- a/Controller/ArgumentResolverInterface.php +++ b/Controller/ArgumentResolverInterface.php @@ -24,7 +24,7 @@ interface ArgumentResolverInterface /** * Returns the arguments to pass to the controller. * - * @return array An array of arguments to pass to the controller + * @return array * * @throws \RuntimeException When no value could be provided for a required argument */ diff --git a/Controller/ControllerResolver.php b/Controller/ControllerResolver.php index cffe45904d..8abbadd48b 100644 --- a/Controller/ControllerResolver.php +++ b/Controller/ControllerResolver.php @@ -47,7 +47,7 @@ public function getController(Request $request) if (isset($controller[0]) && \is_string($controller[0]) && isset($controller[1])) { try { $controller[0] = $this->instantiateController($controller[0]); - } catch (\Error | \LogicException $e) { + } catch (\Error|\LogicException $e) { try { // We cannot just check is_callable but have to use reflection because a non-static method // can still be called statically in PHP but we don't want that. This is deprecated in PHP 7, so we @@ -98,13 +98,13 @@ public function getController(Request $request) /** * Returns a callable for the given controller. * - * @return callable A PHP callable + * @return callable * * @throws \InvalidArgumentException When the controller cannot be created */ protected function createController(string $controller) { - if (false === strpos($controller, '::')) { + if (!str_contains($controller, '::')) { $controller = $this->instantiateController($controller); if (!\is_callable($controller)) { @@ -114,11 +114,11 @@ protected function createController(string $controller) return $controller; } - list($class, $method) = explode('::', $controller, 2); + [$class, $method] = explode('::', $controller, 2); try { $controller = [$this->instantiateController($class), $method]; - } catch (\Error | \LogicException $e) { + } catch (\Error|\LogicException $e) { try { if ((new \ReflectionMethod($class, $method))->isStatic()) { return $class.'::'.$method; @@ -150,7 +150,7 @@ protected function instantiateController(string $class) private function getControllerError($callable): string { if (\is_string($callable)) { - if (false !== strpos($callable, '::')) { + if (str_contains($callable, '::')) { $callable = explode('::', $callable, 2); } else { return sprintf('Function "%s" does not exist.', $callable); @@ -172,7 +172,7 @@ private function getControllerError($callable): string return 'Invalid array callable, expected [controller, method].'; } - list($controller, $method) = $callable; + [$controller, $method] = $callable; if (\is_string($controller) && !class_exists($controller)) { return sprintf('Class "%s" does not exist.', $controller); @@ -191,7 +191,7 @@ private function getControllerError($callable): string foreach ($collection as $item) { $lev = levenshtein($method, $item); - if ($lev <= \strlen($method) / 3 || false !== strpos($item, $method)) { + if ($lev <= \strlen($method) / 3 || str_contains($item, $method)) { $alternatives[] = $item; } } diff --git a/ControllerMetadata/ArgumentMetadata.php b/ControllerMetadata/ArgumentMetadata.php index 6fc7e70344..1a9ebc0c3a 100644 --- a/ControllerMetadata/ArgumentMetadata.php +++ b/ControllerMetadata/ArgumentMetadata.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\ControllerMetadata; +use Symfony\Component\HttpKernel\Attribute\ArgumentInterface; + /** * Responsible for storing metadata of an argument. * @@ -18,14 +20,20 @@ */ class ArgumentMetadata { + public const IS_INSTANCEOF = 2; + private $name; private $type; private $isVariadic; private $hasDefaultValue; private $defaultValue; private $isNullable; + private $attributes; - public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false) + /** + * @param object[] $attributes + */ + public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, $attributes = []) { $this->name = $name; $this->type = $type; @@ -33,6 +41,13 @@ public function __construct(string $name, ?string $type, bool $isVariadic, bool $this->hasDefaultValue = $hasDefaultValue; $this->defaultValue = $defaultValue; $this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue); + + if (null === $attributes || $attributes instanceof ArgumentInterface) { + trigger_deprecation('symfony/http-kernel', '5.3', 'The "%s" constructor expects an array of PHP attributes as last argument, %s given.', __CLASS__, get_debug_type($attributes)); + $attributes = $attributes ? [$attributes] : []; + } + + $this->attributes = $attributes; } /** @@ -104,4 +119,45 @@ public function getDefaultValue() return $this->defaultValue; } + + /** + * Returns the attribute (if any) that was set on the argument. + */ + public function getAttribute(): ?ArgumentInterface + { + trigger_deprecation('symfony/http-kernel', '5.3', 'Method "%s()" is deprecated, use "getAttributes()" instead.', __METHOD__); + + if (!$this->attributes) { + return null; + } + + return $this->attributes[0] instanceof ArgumentInterface ? $this->attributes[0] : null; + } + + /** + * @return object[] + */ + public function getAttributes(string $name = null, int $flags = 0): array + { + if (!$name) { + return $this->attributes; + } + + $attributes = []; + if ($flags & self::IS_INSTANCEOF) { + foreach ($this->attributes as $attribute) { + if ($attribute instanceof $name) { + $attributes[] = $attribute; + } + } + } else { + foreach ($this->attributes as $attribute) { + if (\get_class($attribute) === $name) { + $attributes[] = $attribute; + } + } + } + + return $attributes; + } } diff --git a/ControllerMetadata/ArgumentMetadataFactory.php b/ControllerMetadata/ArgumentMetadataFactory.php index 05a68229a3..85bb805f34 100644 --- a/ControllerMetadata/ArgumentMetadataFactory.php +++ b/ControllerMetadata/ArgumentMetadataFactory.php @@ -27,14 +27,28 @@ public function createArgumentMetadata($controller): array if (\is_array($controller)) { $reflection = new \ReflectionMethod($controller[0], $controller[1]); + $class = $reflection->class; } elseif (\is_object($controller) && !$controller instanceof \Closure) { - $reflection = (new \ReflectionObject($controller))->getMethod('__invoke'); + $reflection = new \ReflectionMethod($controller, '__invoke'); + $class = $reflection->class; } else { $reflection = new \ReflectionFunction($controller); + if ($class = str_contains($reflection->name, '{closure}') ? null : (\PHP_VERSION_ID >= 80111 ? $reflection->getClosureCalledClass() : $reflection->getClosureScopeClass())) { + $class = $class->name; + } } foreach ($reflection->getParameters() as $param) { - $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull()); + $attributes = []; + if (\PHP_VERSION_ID >= 80000) { + foreach ($param->getAttributes() as $reflectionAttribute) { + if (class_exists($reflectionAttribute->getName())) { + $attributes[] = $reflectionAttribute->newInstance(); + } + } + } + + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $class), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes); } return $arguments; @@ -43,20 +57,19 @@ public function createArgumentMetadata($controller): array /** * Returns an associated type to the given parameter if available. */ - private function getType(\ReflectionParameter $parameter, \ReflectionFunctionAbstract $function): ?string + private function getType(\ReflectionParameter $parameter, ?string $class): ?string { if (!$type = $parameter->getType()) { return null; } $name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; - if ($function instanceof \ReflectionMethod) { - $lcName = strtolower($name); - switch ($lcName) { + if (null !== $class) { + switch (strtolower($name)) { case 'self': - return $function->getDeclaringClass()->name; + return $class; case 'parent': - return ($parent = $function->getDeclaringClass()->getParentClass()) ? $parent->name : null; + return get_parent_class($class) ?: null; } } diff --git a/ControllerMetadata/ArgumentMetadataFactoryInterface.php b/ControllerMetadata/ArgumentMetadataFactoryInterface.php index 6ea179d783..a34befc22d 100644 --- a/ControllerMetadata/ArgumentMetadataFactoryInterface.php +++ b/ControllerMetadata/ArgumentMetadataFactoryInterface.php @@ -19,7 +19,7 @@ interface ArgumentMetadataFactoryInterface { /** - * @param mixed $controller The controller to resolve the arguments for + * @param string|object|array $controller The controller to resolve the arguments for * * @return ArgumentMetadata[] */ diff --git a/DataCollector/AjaxDataCollector.php b/DataCollector/AjaxDataCollector.php index 7b38ed5d79..fda6a4eaaa 100644 --- a/DataCollector/AjaxDataCollector.php +++ b/DataCollector/AjaxDataCollector.php @@ -15,8 +15,6 @@ use Symfony\Component\HttpFoundation\Response; /** - * AjaxDataCollector. - * * @author Bart van den Burg * * @final @@ -33,7 +31,7 @@ public function reset() // all collecting is done client side } - public function getName() + public function getName(): string { return 'ajax'; } diff --git a/DataCollector/ConfigDataCollector.php b/DataCollector/ConfigDataCollector.php index ba0a3eec6e..9819507aab 100644 --- a/DataCollector/ConfigDataCollector.php +++ b/DataCollector/ConfigDataCollector.php @@ -42,19 +42,26 @@ public function setKernel(KernelInterface $kernel = null) */ public function collect(Request $request, Response $response, \Throwable $exception = null) { + $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE); + $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE); + $this->data = [ 'token' => $response->headers->get('X-Debug-Token'), 'symfony_version' => Kernel::VERSION, - 'symfony_state' => 'unknown', + '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'), + 'symfony_eol' => $eol->format('F Y'), 'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a', 'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a', 'php_version' => \PHP_VERSION, 'php_architecture' => \PHP_INT_SIZE * 8, - 'php_intl_locale' => class_exists('Locale', false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', + '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'), - 'apcu_enabled' => \extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN), - 'zend_opcache_enabled' => \extension_loaded('Zend OPcache') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN), + 'apcu_enabled' => \extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN), + 'zend_opcache_enabled' => \extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN), 'bundles' => [], 'sapi_name' => \PHP_SAPI, ]; @@ -63,14 +70,6 @@ public function collect(Request $request, Response $response, \Throwable $except foreach ($this->kernel->getBundles() as $name => $bundle) { $this->data['bundles'][$name] = new ClassStub(\get_class($bundle)); } - - $this->data['symfony_state'] = $this->determineSymfonyState(); - $this->data['symfony_minor_version'] = sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION); - $this->data['symfony_lts'] = 4 === Kernel::MINOR_VERSION; - $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE); - $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE); - $this->data['symfony_eom'] = $eom->format('F Y'); - $this->data['symfony_eol'] = $eol->format('F Y'); } if (preg_match('~^(\d+(?:\.\d+)*)(.+)?$~', $this->data['php_version'], $matches) && isset($matches[2])) { @@ -94,20 +93,16 @@ public function lateCollect() /** * Gets the token. - * - * @return string|null The token */ - public function getToken() + public function getToken(): ?string { return $this->data['token']; } /** * Gets the Symfony version. - * - * @return string The Symfony version */ - public function getSymfonyVersion() + public function getSymfonyVersion(): string { return $this->data['symfony_version']; } @@ -117,7 +112,7 @@ public function getSymfonyVersion() * * @return string One of: unknown, dev, stable, eom, eol */ - public function getSymfonyState() + public function getSymfonyState(): string { return $this->data['symfony_state']; } @@ -125,10 +120,8 @@ public function getSymfonyState() /** * Returns the minor Symfony version used (without patch numbers of extra * suffix like "RC", "beta", etc.). - * - * @return string */ - public function getSymfonyMinorVersion() + public function getSymfonyMinorVersion(): string { return $this->data['symfony_minor_version']; } @@ -142,77 +135,61 @@ public function isSymfonyLts(): bool } /** - * Returns the human redable date when this Symfony version ends its + * Returns the human readable date when this Symfony version ends its * maintenance period. - * - * @return string */ - public function getSymfonyEom() + public function getSymfonyEom(): string { return $this->data['symfony_eom']; } /** - * Returns the human redable date when this Symfony version reaches its + * Returns the human readable date when this Symfony version reaches its * "end of life" and won't receive bugs or security fixes. - * - * @return string */ - public function getSymfonyEol() + public function getSymfonyEol(): string { return $this->data['symfony_eol']; } /** * Gets the PHP version. - * - * @return string The PHP version */ - public function getPhpVersion() + public function getPhpVersion(): string { return $this->data['php_version']; } /** * Gets the PHP version extra part. - * - * @return string|null The extra part */ - public function getPhpVersionExtra() + public function getPhpVersionExtra(): ?string { - return isset($this->data['php_version_extra']) ? $this->data['php_version_extra'] : null; + return $this->data['php_version_extra'] ?? null; } /** * @return int The PHP architecture as number of bits (e.g. 32 or 64) */ - public function getPhpArchitecture() + public function getPhpArchitecture(): int { return $this->data['php_architecture']; } - /** - * @return string - */ - public function getPhpIntlLocale() + public function getPhpIntlLocale(): string { return $this->data['php_intl_locale']; } - /** - * @return string - */ - public function getPhpTimezone() + public function getPhpTimezone(): string { return $this->data['php_timezone']; } /** * Gets the environment. - * - * @return string The environment */ - public function getEnv() + public function getEnv(): string { return $this->data['env']; } @@ -220,7 +197,7 @@ public function getEnv() /** * Returns true if the debug is enabled. * - * @return bool true if debug is enabled, false otherwise + * @return bool|string true if debug is enabled, false otherwise or a string if no kernel was set */ public function isDebug() { @@ -229,30 +206,24 @@ public function isDebug() /** * Returns true if the XDebug is enabled. - * - * @return bool true if XDebug is enabled, false otherwise */ - public function hasXDebug() + public function hasXDebug(): bool { return $this->data['xdebug_enabled']; } /** * Returns true if APCu is enabled. - * - * @return bool true if APCu is enabled, false otherwise */ - public function hasApcu() + public function hasApcu(): bool { return $this->data['apcu_enabled']; } /** * Returns true if Zend OPcache is enabled. - * - * @return bool true if Zend OPcache is enabled, false otherwise */ - public function hasZendOpcache() + public function hasZendOpcache(): bool { return $this->data['zend_opcache_enabled']; } @@ -264,10 +235,8 @@ public function getBundles() /** * Gets the PHP SAPI name. - * - * @return string The environment */ - public function getSapiName() + public function getSapiName(): string { return $this->data['sapi_name']; } @@ -275,7 +244,7 @@ public function getSapiName() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'config'; } diff --git a/DataCollector/DataCollectorInterface.php b/DataCollector/DataCollectorInterface.php index 30ab7cc70c..1cb865fd66 100644 --- a/DataCollector/DataCollectorInterface.php +++ b/DataCollector/DataCollectorInterface.php @@ -30,7 +30,7 @@ public function collect(Request $request, Response $response, \Throwable $except /** * Returns the name of the collector. * - * @return string The collector name + * @return string */ public function getName(); } diff --git a/DataCollector/DumpDataCollector.php b/DataCollector/DumpDataCollector.php index ba45a2ddce..08026e5622 100644 --- a/DataCollector/DumpDataCollector.php +++ b/DataCollector/DumpDataCollector.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\VarCloner; @@ -43,13 +44,15 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface private $sourceContextProvider; /** + * @param string|FileLinkFormatter|null $fileLinkFormat * @param DataDumperInterface|Connection|null $dumper */ public function __construct(Stopwatch $stopwatch = null, $fileLinkFormat = null, string $charset = null, RequestStack $requestStack = null, $dumper = null) { + $fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->stopwatch = $stopwatch; - $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); - $this->charset = $charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'; + $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; @@ -75,7 +78,7 @@ public function dump(Data $data) $this->stopwatch->start('dump'); } - list('name' => $name, 'file' => $file, 'line' => $line, 'file_excerpt' => $fileExcerpt) = $this->sourceContextProvider->getContext(); + ['name' => $name, 'file' => $file, 'line' => $line, 'file_excerpt' => $fileExcerpt] = $this->sourceContextProvider->getContext(); if ($this->dumper instanceof Connection) { if (!$this->dumper->write($data)) { @@ -105,7 +108,7 @@ public function collect(Request $request, Response $response, \Throwable $except } // Sub-requests and programmatic calls stay in the collected profile. - if ($this->dumper || ($this->requestStack && $this->requestStack->getMasterRequest() !== $request) || $request->isXmlHttpRequest() || $request->headers->has('Origin')) { + if ($this->dumper || ($this->requestStack && $this->requestStack->getMainRequest() !== $request) || $request->isXmlHttpRequest() || $request->headers->has('Origin')) { return; } @@ -113,11 +116,11 @@ public function collect(Request $request, Response $response, \Throwable $except if (!$this->requestStack || !$response->headers->has('X-Debug-Token') || $response->isRedirection() - || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html')) || 'html' !== $request->getRequestFormat() || false === strripos($response->getContent(), '') ) { - if ($response->headers->has('Content-Type') && false !== strpos($response->headers->get('Content-Type'), 'html')) { + 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 { @@ -176,8 +179,13 @@ public function __wakeup() $charset = array_pop($this->data); $fileLinkFormat = array_pop($this->data); $this->dataCount = \count($this->data); + foreach ($this->data as $dump) { + if (!\is_string($dump['name']) || !\is_string($dump['file']) || !\is_int($dump['line'])) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + } - self::__construct($this->stopwatch, $fileLinkFormat, $charset); + self::__construct($this->stopwatch, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null); } public function getDumpsCount(): int @@ -185,9 +193,9 @@ public function getDumpsCount(): int return $this->dataCount; } - public function getDumps($format, $maxDepthLimit = -1, $maxItemsPerDepth = -1): array + public function getDumps(string $format, int $maxDepthLimit = -1, int $maxItemsPerDepth = -1): array { - $data = fopen('php://memory', 'r+b'); + $data = fopen('php://memory', 'r+'); if ('html' === $format) { $dumper = new HtmlDumper($data, $this->charset); @@ -225,7 +233,7 @@ public function __destruct() $h = headers_list(); $i = \count($h); - array_unshift($h, 'Content-Type: '.ini_get('default_mimetype')); + array_unshift($h, 'Content-Type: '.\ini_get('default_mimetype')); while (0 !== stripos($h[$i], 'Content-Type:')) { --$i; } @@ -250,7 +258,7 @@ public function __destruct() } } - private function doDump(DataDumperInterface $dumper, $data, string $name, string $file, int $line) + private function doDump(DataDumperInterface $dumper, Data $data, string $name, string $file, int $line) { if ($dumper instanceof CliDumper) { $contextDumper = function ($name, $file, $line, $fmt) { diff --git a/DataCollector/EventDataCollector.php b/DataCollector/EventDataCollector.php index 27930fea09..a813553364 100644 --- a/DataCollector/EventDataCollector.php +++ b/DataCollector/EventDataCollector.php @@ -15,12 +15,11 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Service\ResetInterface; /** - * EventDataCollector. - * * @author Fabien Potencier * * @final @@ -42,7 +41,7 @@ public function __construct(EventDispatcherInterface $dispatcher = null, Request */ public function collect(Request $request, Response $response, \Throwable $exception = null) { - $this->currentRequest = $this->requestStack && $this->requestStack->getMasterRequest() !== $request ? $request : null; + $this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null; $this->data = [ 'called_listeners' => [], 'not_called_listeners' => [], @@ -71,8 +70,6 @@ public function lateCollect() } /** - * Sets the called listeners. - * * @param array $listeners An array of called listeners * * @see TraceableEventDispatcher @@ -83,11 +80,9 @@ public function setCalledListeners(array $listeners) } /** - * Gets the called listeners. - * - * @return array An array of called listeners - * * @see TraceableEventDispatcher + * + * @return array|Data */ public function getCalledListeners() { @@ -95,8 +90,6 @@ public function getCalledListeners() } /** - * Sets the not called listeners. - * * @see TraceableEventDispatcher */ public function setNotCalledListeners(array $listeners) @@ -105,11 +98,9 @@ public function setNotCalledListeners(array $listeners) } /** - * Gets the not called listeners. - * - * @return array - * * @see TraceableEventDispatcher + * + * @return array|Data */ public function getNotCalledListeners() { @@ -117,8 +108,6 @@ public function getNotCalledListeners() } /** - * Sets the orphaned events. - * * @param array $events An array of orphaned events * * @see TraceableEventDispatcher @@ -129,11 +118,9 @@ public function setOrphanedEvents(array $events) } /** - * Gets the orphaned events. - * - * @return array An array of orphaned events - * * @see TraceableEventDispatcher + * + * @return array|Data */ public function getOrphanedEvents() { @@ -143,7 +130,7 @@ public function getOrphanedEvents() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'events'; } diff --git a/DataCollector/ExceptionDataCollector.php b/DataCollector/ExceptionDataCollector.php index 5ff13f71b8..14bbbb364b 100644 --- a/DataCollector/ExceptionDataCollector.php +++ b/DataCollector/ExceptionDataCollector.php @@ -16,8 +16,6 @@ use Symfony\Component\HttpFoundation\Response; /** - * ExceptionDataCollector. - * * @author Fabien Potencier * * @final @@ -44,19 +42,12 @@ public function reset() $this->data = []; } - /** - * Checks if the exception is not null. - * - * @return bool true if the exception is not null, false otherwise - */ - public function hasException() + public function hasException(): bool { return isset($this->data['exception']); } /** - * Gets the exception. - * * @return \Exception|FlattenException */ public function getException() @@ -64,42 +55,22 @@ public function getException() return $this->data['exception']; } - /** - * Gets the exception message. - * - * @return string The exception message - */ - public function getMessage() + public function getMessage(): string { return $this->data['exception']->getMessage(); } - /** - * Gets the exception code. - * - * @return int The exception code - */ - public function getCode() + public function getCode(): int { return $this->data['exception']->getCode(); } - /** - * Gets the status code. - * - * @return int The status code - */ - public function getStatusCode() + public function getStatusCode(): int { return $this->data['exception']->getStatusCode(); } - /** - * Gets the exception trace. - * - * @return array The exception trace - */ - public function getTrace() + public function getTrace(): array { return $this->data['exception']->getTrace(); } @@ -107,7 +78,7 @@ public function getTrace() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'exception'; } diff --git a/DataCollector/LoggerDataCollector.php b/DataCollector/LoggerDataCollector.php index 504319aea9..2bbd2a039e 100644 --- a/DataCollector/LoggerDataCollector.php +++ b/DataCollector/LoggerDataCollector.php @@ -18,8 +18,6 @@ use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; /** - * LogDataCollector. - * * @author Fabien Potencier * * @final @@ -30,8 +28,9 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInte private $containerPathPrefix; private $currentRequest; private $requestStack; + private $processedLogs; - public function __construct($logger = null, string $containerPathPrefix = null, RequestStack $requestStack = null) + public function __construct(object $logger = null, string $containerPathPrefix = null, RequestStack $requestStack = null) { if (null !== $logger && $logger instanceof DebugLoggerInterface) { $this->logger = $logger; @@ -46,7 +45,7 @@ public function __construct($logger = null, string $containerPathPrefix = null, */ public function collect(Request $request, Response $response, \Throwable $exception = null) { - $this->currentRequest = $this->requestStack && $this->requestStack->getMasterRequest() !== $request ? $request : null; + $this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null; } /** @@ -79,32 +78,108 @@ public function lateCollect() public function getLogs() { - return isset($this->data['logs']) ? $this->data['logs'] : []; + return $this->data['logs'] ?? []; + } + + public function getProcessedLogs() + { + if (null !== $this->processedLogs) { + return $this->processedLogs; + } + + $rawLogs = $this->getLogs(); + if ([] === $rawLogs) { + return $this->processedLogs = $rawLogs; + } + + $logs = []; + foreach ($this->getLogs()->getValue() as $rawLog) { + $rawLogData = $rawLog->getValue(); + + if ($rawLogData['priority']->getValue() > 300) { + $logType = 'error'; + } elseif (isset($rawLogData['scream']) && false === $rawLogData['scream']->getValue()) { + $logType = 'deprecation'; + } elseif (isset($rawLogData['scream']) && true === $rawLogData['scream']->getValue()) { + $logType = 'silenced'; + } else { + $logType = 'regular'; + } + + $logs[] = [ + 'type' => $logType, + 'errorCount' => $rawLog['errorCount'] ?? 1, + 'timestamp' => $rawLogData['timestamp_rfc3339']->getValue(), + 'priority' => $rawLogData['priority']->getValue(), + 'priorityName' => $rawLogData['priorityName']->getValue(), + 'channel' => $rawLogData['channel']->getValue(), + 'message' => $rawLogData['message'], + 'context' => $rawLogData['context'], + ]; + } + + // sort logs from oldest to newest + usort($logs, static function ($logA, $logB) { + return $logA['timestamp'] <=> $logB['timestamp']; + }); + + return $this->processedLogs = $logs; + } + + public function getFilters() + { + $filters = [ + 'channel' => [], + 'priority' => [ + 'Debug' => 100, + 'Info' => 200, + 'Notice' => 250, + 'Warning' => 300, + 'Error' => 400, + 'Critical' => 500, + 'Alert' => 550, + 'Emergency' => 600, + ], + ]; + + $allChannels = []; + foreach ($this->getProcessedLogs() as $log) { + if ('' === trim($log['channel'] ?? '')) { + continue; + } + + $allChannels[] = $log['channel']; + } + $channels = array_unique($allChannels); + sort($channels); + $filters['channel'] = $channels; + + return $filters; } public function getPriorities() { - return isset($this->data['priorities']) ? $this->data['priorities'] : []; + return $this->data['priorities'] ?? []; } public function countErrors() { - return isset($this->data['error_count']) ? $this->data['error_count'] : 0; + return $this->data['error_count'] ?? 0; } public function countDeprecations() { - return isset($this->data['deprecation_count']) ? $this->data['deprecation_count'] : 0; + return $this->data['deprecation_count'] ?? 0; } public function countWarnings() { - return isset($this->data['warning_count']) ? $this->data['warning_count'] : 0; + return $this->data['warning_count'] ?? 0; } public function countScreams() { - return isset($this->data['scream_count']) ? $this->data['scream_count'] : 0; + return $this->data['scream_count'] ?? 0; } public function getCompilerLogs() @@ -115,14 +190,14 @@ public function getCompilerLogs() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'logger'; } private function getContainerDeprecationLogs(): array { - if (null === $this->containerPathPrefix || !file_exists($file = $this->containerPathPrefix.'Deprecations.log')) { + if (null === $this->containerPathPrefix || !is_file($file = $this->containerPathPrefix.'Deprecations.log')) { return []; } @@ -135,6 +210,7 @@ private function getContainerDeprecationLogs(): array foreach (unserialize($logContent) as $log) { $log['context'] = ['exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'], $log['trace'], $log['count'])]; $log['timestamp'] = $bootTime; + $log['timestamp_rfc3339'] = (new \DateTimeImmutable())->setTimestamp($bootTime)->format(\DateTimeInterface::RFC3339_EXTENDED); $log['priority'] = 100; $log['priorityName'] = 'DEBUG'; $log['channel'] = null; @@ -148,7 +224,7 @@ private function getContainerDeprecationLogs(): array private function getContainerCompilerLogs(string $compilerLogsFilepath = null): array { - if (!file_exists($compilerLogsFilepath)) { + if (!is_file($compilerLogsFilepath)) { return []; } diff --git a/DataCollector/MemoryDataCollector.php b/DataCollector/MemoryDataCollector.php index 37302128ad..53a1f9e448 100644 --- a/DataCollector/MemoryDataCollector.php +++ b/DataCollector/MemoryDataCollector.php @@ -15,8 +15,6 @@ use Symfony\Component\HttpFoundation\Response; /** - * MemoryDataCollector. - * * @author Fabien Potencier * * @final @@ -43,7 +41,7 @@ public function reset() { $this->data = [ 'memory' => 0, - 'memory_limit' => $this->convertToBytes(ini_get('memory_limit')), + 'memory_limit' => $this->convertToBytes(\ini_get('memory_limit')), ]; } @@ -55,29 +53,19 @@ public function lateCollect() $this->updateMemoryUsage(); } - /** - * Gets the memory. - * - * @return int The memory - */ - public function getMemory() + public function getMemory(): int { return $this->data['memory']; } /** - * Gets the PHP memory limit. - * - * @return int The memory limit + * @return int|float */ public function getMemoryLimit() { return $this->data['memory_limit']; } - /** - * Updates the memory usage data. - */ public function updateMemoryUsage() { $this->data['memory'] = memory_get_peak_usage(true); @@ -86,7 +74,7 @@ public function updateMemoryUsage() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'memory'; } @@ -102,9 +90,9 @@ private function convertToBytes(string $memoryLimit) $memoryLimit = strtolower($memoryLimit); $max = strtolower(ltrim($memoryLimit, '+')); - if (0 === strpos($max, '0x')) { + if (str_starts_with($max, '0x')) { $max = \intval($max, 16); - } elseif (0 === strpos($max, '0')) { + } elseif (str_starts_with($max, '0')) { $max = \intval($max, 8); } else { $max = (int) $max; diff --git a/DataCollector/RequestDataCollector.php b/DataCollector/RequestDataCollector.php index da3fa419d6..5717000f29 100644 --- a/DataCollector/RequestDataCollector.php +++ b/DataCollector/RequestDataCollector.php @@ -15,10 +15,14 @@ use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\VarDumper\Cloner\Data; /** * @author Fabien Potencier @@ -27,11 +31,17 @@ */ class RequestDataCollector extends DataCollector implements EventSubscriberInterface, LateDataCollectorInterface { - protected $controllers; + /** + * @var \SplObjectStorage + */ + private $controllers; + private $sessionUsages = []; + private $requestStack; - public function __construct() + public function __construct(RequestStack $requestStack = null) { $this->controllers = new \SplObjectStorage(); + $this->requestStack = $requestStack; } /** @@ -51,12 +61,7 @@ public function collect(Request $request, Response $response, \Throwable $except } } - try { - $content = $request->getContent(); - } catch (\LogicException $e) { - // the user already got the request content as a resource - $content = false; - } + $content = $request->getContent(); $sessionMetadata = []; $sessionAttributes = []; @@ -89,9 +94,8 @@ public function collect(Request $request, Response $response, \Throwable $except $this->data = [ 'method' => $request->getMethod(), 'format' => $request->getRequestFormat(), - 'content' => $content, 'content_type' => $response->headers->get('Content-Type', 'text/html'), - 'status_text' => isset(Response::$statusTexts[$statusCode]) ? Response::$statusTexts[$statusCode] : '', + 'status_text' => Response::$statusTexts[$statusCode] ?? '', 'status_code' => $statusCode, 'request_query' => $request->query->all(), 'request_request' => $request->request->all(), @@ -105,6 +109,8 @@ public function collect(Request $request, Response $response, \Throwable $except 'response_cookies' => $responseCookies, 'session_metadata' => $sessionMetadata, 'session_attributes' => $sessionAttributes, + 'session_usages' => array_values($this->sessionUsages), + 'stateless_check' => $this->requestStack && ($mainRequest = $this->requestStack->getMainRequest()) && $mainRequest->attributes->get('_stateless', false), 'flashes' => $flashes, 'path_info' => $request->getPathInfo(), 'controller' => 'n/a', @@ -121,9 +127,13 @@ public function collect(Request $request, Response $response, \Throwable $except } if (isset($this->data['request_request']['_password'])) { + $encodedPassword = rawurlencode($this->data['request_request']['_password']); + $content = str_replace('_password='.$encodedPassword, '_password=******', $content); $this->data['request_request']['_password'] = '******'; } + $this->data['content'] = $content; + foreach ($this->data as $key => $value) { if (!\is_array($value)) { continue; @@ -153,7 +163,7 @@ public function collect(Request $request, Response $response, \Throwable $except 'method' => $request->getMethod(), 'controller' => $this->parseController($request->attributes->get('_controller')), 'status_code' => $statusCode, - 'status_text' => Response::$statusTexts[(int) $statusCode], + 'status_text' => Response::$statusTexts[$statusCode], ]), 0, '/', null, $request->isSecure(), true, false, 'lax' )); @@ -175,6 +185,7 @@ public function reset() { $this->data = []; $this->controllers = new \SplObjectStorage(); + $this->sessionUsages = []; } public function getMethod() @@ -207,12 +218,12 @@ public function getRequestHeaders() return new ParameterBag($this->data['request_headers']->getValue()); } - public function getRequestServer($raw = false) + public function getRequestServer(bool $raw = false) { return new ParameterBag($this->data['request_server']->getValue($raw)); } - public function getRequestCookies($raw = false) + public function getRequestCookies(bool $raw = false) { return new ParameterBag($this->data['request_cookies']->getValue($raw)); } @@ -242,6 +253,16 @@ public function getSessionAttributes() return $this->data['session_attributes']->getValue(); } + public function getStatelessCheck() + { + return $this->data['stateless_check']; + } + + public function getSessionUsages() + { + return $this->data['session_usages']; + } + public function getFlashes() { return $this->data['flashes']->getValue(); @@ -298,10 +319,8 @@ public function getDotenvVars() * Gets the route name. * * The _route request attributes is automatically set by the Router Matcher. - * - * @return string The route */ - public function getRoute() + public function getRoute(): string { return $this->data['route']; } @@ -315,10 +334,8 @@ public function getIdentifier() * Gets the route parameters. * * The _route_params request attributes is automatically set by the RouterListener. - * - * @return array The parameters */ - public function getRouteParams() + public function getRouteParams(): array { return isset($this->data['request_attributes']['_route_params']) ? $this->data['request_attributes']['_route_params']->getValue() : []; } @@ -326,8 +343,8 @@ public function getRouteParams() /** * Gets the parsed controller. * - * @return array|string The controller as a string or array of data - * with keys 'class', 'method', 'file' and 'line' + * @return array|string|Data The controller as a string or array of data + * with keys 'class', 'method', 'file' and 'line' */ public function getController() { @@ -337,17 +354,17 @@ public function getController() /** * Gets the previous request attributes. * - * @return array|bool A legacy array of data from the previous redirection response - * or false otherwise + * @return array|Data|false A legacy array of data from the previous redirection response + * or false otherwise */ public function getRedirect() { - return isset($this->data['redirect']) ? $this->data['redirect'] : false; + return $this->data['redirect'] ?? false; } public function getForwardToken() { - return isset($this->data['forward_token']) ? $this->data['forward_token'] : null; + return $this->data['forward_token'] ?? null; } public function onKernelController(ControllerEvent $event) @@ -357,7 +374,7 @@ public function onKernelController(ControllerEvent $event) public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -366,7 +383,7 @@ public function onKernelResponse(ResponseEvent $event) } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER => 'onKernelController', @@ -377,21 +394,50 @@ public static function getSubscribedEvents() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'request'; } + public function collectSessionUsage(): void + { + $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + + $traceEndIndex = \count($trace) - 1; + for ($i = $traceEndIndex; $i > 0; --$i) { + if (null !== ($class = $trace[$i]['class'] ?? null) && (is_subclass_of($class, SessionInterface::class) || is_subclass_of($class, SessionBagInterface::class))) { + $traceEndIndex = $i; + break; + } + } + + if ((\count($trace) - 1) === $traceEndIndex) { + return; + } + + // Remove part of the backtrace that belongs to session only + 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']); + if (!\array_key_exists($name, $this->sessionUsages)) { + $this->sessionUsages[$name] = [ + 'name' => $name, + 'file' => $trace[0]['file'], + 'line' => $trace[0]['line'], + 'trace' => $trace, + ]; + } + } + /** - * Parse a controller. - * - * @param mixed $controller The controller to parse + * @param string|object|array|null $controller The controller to parse * * @return array|string An array of controller data or a simple string */ - protected function parseController($controller) + private function parseController($controller) { - if (\is_string($controller) && false !== strpos($controller, '::')) { + if (\is_string($controller) && str_contains($controller, '::')) { $controller = explode('::', $controller); } @@ -428,12 +474,12 @@ protected function parseController($controller) 'line' => $r->getStartLine(), ]; - if (false !== strpos($r->name, '{closure}')) { + if (str_contains($r->name, '{closure}')) { return $controller; } $controller['method'] = $r->name; - if ($class = $r->getClosureScopeClass()) { + if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { $controller['class'] = $class->name; } else { return $r->name; diff --git a/DataCollector/RouterDataCollector.php b/DataCollector/RouterDataCollector.php index 5ed697048b..372ede0378 100644 --- a/DataCollector/RouterDataCollector.php +++ b/DataCollector/RouterDataCollector.php @@ -22,7 +22,7 @@ class RouterDataCollector extends DataCollector { /** - * @var \SplObjectStorage + * @var \SplObjectStorage */ protected $controllers; @@ -83,7 +83,7 @@ public function getRedirect() } /** - * @return string|null The target URL + * @return string|null */ public function getTargetUrl() { @@ -91,7 +91,7 @@ public function getTargetUrl() } /** - * @return string|null The target route + * @return string|null */ public function getTargetRoute() { diff --git a/DataCollector/TimeDataCollector.php b/DataCollector/TimeDataCollector.php index 4e95603fb8..43799060f6 100644 --- a/DataCollector/TimeDataCollector.php +++ b/DataCollector/TimeDataCollector.php @@ -24,8 +24,8 @@ */ class TimeDataCollector extends DataCollector implements LateDataCollectorInterface { - protected $kernel; - protected $stopwatch; + private $kernel; + private $stopwatch; public function __construct(KernelInterface $kernel = null, Stopwatch $stopwatch = null) { @@ -45,7 +45,7 @@ public function collect(Request $request, Response $response, \Throwable $except } $this->data = [ - 'token' => $response->headers->get('X-Debug-Token'), + 'token' => $request->attributes->get('_stopwatch_token'), 'start_time' => $startTime * 1000, 'events' => [], 'stopwatch_installed' => class_exists(Stopwatch::class, false), @@ -76,8 +76,6 @@ public function lateCollect() } /** - * Sets the request events. - * * @param StopwatchEvent[] $events The request events */ public function setEvents(array $events) @@ -90,21 +88,17 @@ public function setEvents(array $events) } /** - * Gets the request events. - * - * @return StopwatchEvent[] The request events + * @return StopwatchEvent[] */ - public function getEvents() + public function getEvents(): array { return $this->data['events']; } /** * Gets the request elapsed time. - * - * @return float The elapsed time */ - public function getDuration() + public function getDuration(): float { if (!isset($this->data['events']['__section__'])) { return 0; @@ -119,10 +113,8 @@ public function getDuration() * Gets the initialization time. * * This is the time spent until the beginning of the request handling. - * - * @return float The elapsed time */ - public function getInitTime() + public function getInitTime(): float { if (!isset($this->data['events']['__section__'])) { return 0; @@ -131,20 +123,12 @@ public function getInitTime() return $this->data['events']['__section__']->getOrigin() - $this->getStartTime(); } - /** - * Gets the request time. - * - * @return float - */ - public function getStartTime() + public function getStartTime(): float { return $this->data['start_time']; } - /** - * @return bool whether or not the stopwatch component is installed - */ - public function isStopwatchInstalled() + public function isStopwatchInstalled(): bool { return $this->data['stopwatch_installed']; } @@ -152,7 +136,7 @@ public function isStopwatchInstalled() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'time'; } diff --git a/Debug/FileLinkFormatter.php b/Debug/FileLinkFormatter.php index 87672a9d19..9ac688cc56 100644 --- a/Debug/FileLinkFormatter.php +++ b/Debug/FileLinkFormatter.php @@ -24,18 +24,28 @@ */ class FileLinkFormatter { + private const FORMATS = [ + 'textmate' => 'txmt://open?url=file://%f&line=%l', + 'macvim' => 'mvim://open?url=file://%f&line=%l', + 'emacs' => 'emacs://open?url=file://%f&line=%l', + 'sublime' => 'subl://open?url=file://%f&line=%l', + 'phpstorm' => 'phpstorm://open?file=%f&line=%l', + 'atom' => 'atom://core/open/file?filename=%f&line=%l', + 'vscode' => 'vscode://file/%f:%l', + ]; + private $fileLinkFormat; private $requestStack; private $baseDir; private $urlFormat; /** - * @param string|\Closure $urlFormat the URL format, or a closure that returns it on-demand + * @param string|array|null $fileLinkFormat + * @param string|\Closure $urlFormat the URL format, or a closure that returns it on-demand */ public function __construct($fileLinkFormat = null, RequestStack $requestStack = null, string $baseDir = null, $urlFormat = null) { - $fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); - if ($fileLinkFormat && !\is_array($fileLinkFormat)) { + if (!\is_array($fileLinkFormat) && $fileLinkFormat = (self::FORMATS[$fileLinkFormat] ?? $fileLinkFormat) ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')) { $i = strpos($f = $fileLinkFormat, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f); $fileLinkFormat = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, \PREG_SPLIT_DELIM_CAPTURE); } @@ -50,7 +60,7 @@ public function format(string $file, int $line) { if ($fmt = $this->getFileLinkFormat()) { for ($i = 1; isset($fmt[$i]); ++$i) { - if (0 === strpos($file, $k = $fmt[$i++])) { + if (str_starts_with($file, $k = $fmt[$i++])) { $file = substr_replace($file, $fmt[$i], 0, \strlen($k)); break; } @@ -91,7 +101,7 @@ private function getFileLinkFormat() } if ($this->requestStack && $this->baseDir && $this->urlFormat) { - $request = $this->requestStack->getMasterRequest(); + $request = $this->requestStack->getMainRequest(); if ($request instanceof Request && (!$this->urlFormat instanceof \Closure || $this->urlFormat = ($this->urlFormat)())) { return [ diff --git a/Debug/TraceableEventDispatcher.php b/Debug/TraceableEventDispatcher.php index 1ece493f4e..fd1cf9e584 100644 --- a/Debug/TraceableEventDispatcher.php +++ b/Debug/TraceableEventDispatcher.php @@ -30,6 +30,7 @@ protected function beforeDispatch(string $eventName, object $event) { switch ($eventName) { case KernelEvents::REQUEST: + $event->getRequest()->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); $this->stopwatch->openSection(); break; case KernelEvents::VIEW: @@ -40,8 +41,8 @@ protected function beforeDispatch(string $eventName, object $event) } break; case KernelEvents::TERMINATE: - $token = $event->getResponse()->headers->get('X-Debug-Token'); - if (null === $token) { + $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); + if (null === $sectionId) { break; } // There is a very special case when using built-in AppCache class as kernel wrapper, in the case @@ -50,7 +51,7 @@ protected function beforeDispatch(string $eventName, object $event) // is equal to the [A] debug token. Trying to reopen section with the [B] token throws an exception // which must be caught. try { - $this->stopwatch->openSection($token); + $this->stopwatch->openSection($sectionId); } catch (\LogicException $e) { } break; @@ -67,21 +68,21 @@ protected function afterDispatch(string $eventName, object $event) $this->stopwatch->start('controller', 'section'); break; case KernelEvents::RESPONSE: - $token = $event->getResponse()->headers->get('X-Debug-Token'); - if (null === $token) { + $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); + if (null === $sectionId) { break; } - $this->stopwatch->stopSection($token); + $this->stopwatch->stopSection($sectionId); break; case KernelEvents::TERMINATE: // In the special case described in the `preDispatch` method above, the `$token` section // does not exist, then closing it throws an exception which must be caught. - $token = $event->getResponse()->headers->get('X-Debug-Token'); - if (null === $token) { + $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); + if (null === $sectionId) { break; } try { - $this->stopwatch->stopSection($token); + $this->stopwatch->stopSection($sectionId); } catch (\LogicException $e) { } break; diff --git a/DependencyInjection/AddAnnotatedClassesToCachePass.php b/DependencyInjection/AddAnnotatedClassesToCachePass.php index 5eb833b51d..4bb60b41f7 100644 --- a/DependencyInjection/AddAnnotatedClassesToCachePass.php +++ b/DependencyInjection/AddAnnotatedClassesToCachePass.php @@ -37,13 +37,15 @@ public function __construct(Kernel $kernel) */ public function process(ContainerBuilder $container) { - $annotatedClasses = $this->kernel->getAnnotatedClassesToCompile(); + $annotatedClasses = []; foreach ($container->getExtensions() as $extension) { if ($extension instanceof Extension) { - $annotatedClasses = array_merge($annotatedClasses, $extension->getAnnotatedClassesToCompile()); + $annotatedClasses[] = $extension->getAnnotatedClassesToCompile(); } } + $annotatedClasses = array_merge($this->kernel->getAnnotatedClassesToCompile(), ...$annotatedClasses); + $existingClasses = $this->getClassesInComposerClassMaps(); $annotatedClasses = $container->getParameterBag()->resolveValue($annotatedClasses); @@ -62,7 +64,7 @@ private function expandClasses(array $patterns, array $classes): array // Explicit classes declared in the patterns are returned directly foreach ($patterns as $key => $pattern) { - if ('\\' !== substr($pattern, -1) && false === strpos($pattern, '*')) { + if (!str_ends_with($pattern, '\\') && !str_contains($pattern, '*')) { unset($patterns[$key]); $expanded[] = ltrim($pattern, '\\'); } @@ -127,10 +129,10 @@ private function patternsToRegexps(array $patterns): array private function matchAnyRegexps(string $class, array $regexps): bool { - $isTest = false !== strpos($class, 'Test'); + $isTest = str_contains($class, 'Test'); foreach ($regexps as $regex) { - if ($isTest && false === strpos($regex, 'Test')) { + if ($isTest && !str_contains($regex, 'Test')) { continue; } diff --git a/DependencyInjection/ControllerArgumentValueResolverPass.php b/DependencyInjection/ControllerArgumentValueResolverPass.php index 705c88dbfa..d925ed6b0e 100644 --- a/DependencyInjection/ControllerArgumentValueResolverPass.php +++ b/DependencyInjection/ControllerArgumentValueResolverPass.php @@ -34,6 +34,10 @@ class ControllerArgumentValueResolverPass implements CompilerPassInterface public function __construct(string $argumentResolverService = 'argument_resolver', string $argumentValueResolverTag = 'controller.argument_value_resolver', string $traceableResolverStopwatch = 'debug.stopwatch') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->argumentResolverService = $argumentResolverService; $this->argumentValueResolverTag = $argumentValueResolverTag; $this->traceableResolverStopwatch = $traceableResolverStopwatch; diff --git a/DependencyInjection/Extension.php b/DependencyInjection/Extension.php index db376e6d9f..4090fd822f 100644 --- a/DependencyInjection/Extension.php +++ b/DependencyInjection/Extension.php @@ -25,7 +25,7 @@ abstract class Extension extends BaseExtension /** * Gets the annotated classes to cache. * - * @return array An array of classes + * @return array */ public function getAnnotatedClassesToCompile() { diff --git a/DependencyInjection/FragmentRendererPass.php b/DependencyInjection/FragmentRendererPass.php index 432f767202..f26baeca9d 100644 --- a/DependencyInjection/FragmentRendererPass.php +++ b/DependencyInjection/FragmentRendererPass.php @@ -30,6 +30,10 @@ class FragmentRendererPass implements CompilerPassInterface public function __construct(string $handlerService = 'fragment.handler', string $rendererTag = 'kernel.fragment_renderer') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->handlerService = $handlerService; $this->rendererTag = $rendererTag; } diff --git a/DependencyInjection/LazyLoadingFragmentHandler.php b/DependencyInjection/LazyLoadingFragmentHandler.php index 2ee6737319..f253287045 100644 --- a/DependencyInjection/LazyLoadingFragmentHandler.php +++ b/DependencyInjection/LazyLoadingFragmentHandler.php @@ -23,6 +23,10 @@ class LazyLoadingFragmentHandler extends FragmentHandler { private $container; + + /** + * @var array + */ private $initialized = []; public function __construct(ContainerInterface $container, RequestStack $requestStack, bool $debug = false) diff --git a/DependencyInjection/MergeExtensionConfigurationPass.php b/DependencyInjection/MergeExtensionConfigurationPass.php index 83e1b758de..5f0f0d8dee 100644 --- a/DependencyInjection/MergeExtensionConfigurationPass.php +++ b/DependencyInjection/MergeExtensionConfigurationPass.php @@ -23,6 +23,9 @@ class MergeExtensionConfigurationPass extends BaseMergeExtensionConfigurationPas { private $extensions; + /** + * @param string[] $extensions + */ public function __construct(array $extensions) { $this->extensions = $extensions; diff --git a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index 286f212f9a..3dbaff5641 100644 --- a/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; @@ -23,6 +24,8 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\SessionInterface; /** * Creates the service-locators required by ServiceValueResolver. @@ -38,6 +41,10 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface public function __construct(string $resolverServiceId = 'argument_resolver.service', string $controllerTag = 'controller.service_arguments', string $controllerLocator = 'argument_resolver.controller_locator', string $notTaggedControllerResolverServiceId = 'argument_resolver.not_tagged_controller') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->resolverServiceId = $resolverServiceId; $this->controllerTag = $controllerTag; $this->controllerLocator = $controllerLocator; @@ -106,7 +113,7 @@ public function process(ContainerBuilder $container) if (!isset($methods[$action = strtolower($attributes['action'])])) { throw new InvalidArgumentException(sprintf('Invalid "action" attribute on tag "%s" for service "%s": no public "%s()" method found on class "%s".', $this->controllerTag, $id, $attributes['action'], $class)); } - list($r, $parameters) = $methods[$action]; + [$r, $parameters] = $methods[$action]; $found = false; foreach ($parameters as $p) { @@ -124,14 +131,14 @@ public function process(ContainerBuilder $container) } } - foreach ($methods as list($r, $parameters)) { + foreach ($methods as [$r, $parameters]) { /** @var \ReflectionMethod $r */ // create a per-method map of argument-names to service/type-references $args = []; foreach ($parameters as $p) { /** @var \ReflectionParameter $p */ - $type = ltrim($target = ProxyHelper::getTypeHint($r, $p), '\\'); + $type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\'); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; if (isset($arguments[$r->name][$p->name])) { @@ -143,10 +150,10 @@ public function process(ContainerBuilder $container) } elseif ($p->allowsNull() && !$p->isOptional()) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; } - } elseif (isset($bindings[$bindingName = $type.' $'.$p->name]) || isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) { + } elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p)]) || isset($bindings[$bindingName = '$'.$name]) || isset($bindings[$bindingName = $type])) { $binding = $bindings[$bindingName]; - list($bindingValue, $bindingId, , $bindingType, $bindingFile) = $binding->getValues(); + [$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues(); $binding->setValues([$bindingValue, $bindingId, true, $bindingType, $bindingFile]); if (!$bindingValue instanceof Reference) { @@ -161,11 +168,14 @@ public function process(ContainerBuilder $container) continue; } elseif (!$type || !$autowire || '\\' !== $target[0]) { continue; + } elseif (is_subclass_of($type, \UnitEnum::class)) { + // do not attempt to register enum typed arguments if not already present in bindings + continue; } elseif (!$p->allowsNull()) { $invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE; } - if (Request::class === $type) { + if (Request::class === $type || SessionInterface::class === $type || Response::class === $type) { continue; } @@ -183,7 +193,7 @@ public function process(ContainerBuilder $container) $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); } else { $target = ltrim($target, '\\'); - $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $p->name) : new Reference($target, $invalidBehavior); + $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior); } } // register the maps as a per-method service-locators diff --git a/DependencyInjection/RegisterLocaleAwareServicesPass.php b/DependencyInjection/RegisterLocaleAwareServicesPass.php index 0efb164b72..f0b801b8d6 100644 --- a/DependencyInjection/RegisterLocaleAwareServicesPass.php +++ b/DependencyInjection/RegisterLocaleAwareServicesPass.php @@ -28,6 +28,10 @@ class RegisterLocaleAwareServicesPass implements CompilerPassInterface public function __construct(string $listenerServiceId = 'locale_aware_listener', string $localeAwareTag = 'kernel.locale_aware') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->listenerServiceId = $listenerServiceId; $this->localeAwareTag = $localeAwareTag; } diff --git a/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php b/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php index f69d37619e..2d077a0cb5 100644 --- a/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php +++ b/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php @@ -25,6 +25,10 @@ class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface public function __construct(string $controllerLocator = 'argument_resolver.controller_locator') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->controllerLocator = $controllerLocator; } @@ -42,14 +46,14 @@ public function process(ContainerBuilder $container) } else { // any methods listed for call-at-instantiation cannot be actions $reason = false; - list($id, $action) = explode('::', $controller); + [$id, $action] = explode('::', $controller); if ($container->hasAlias($id)) { continue; } $controllerDef = $container->getDefinition($id); - foreach ($controllerDef->getMethodCalls() as list($method)) { + 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); break; diff --git a/DependencyInjection/ResettableServicePass.php b/DependencyInjection/ResettableServicePass.php index b5e46106a7..2e4cd69270 100644 --- a/DependencyInjection/ResettableServicePass.php +++ b/DependencyInjection/ResettableServicePass.php @@ -27,6 +27,10 @@ class ResettableServicePass implements CompilerPassInterface public function __construct(string $tagName = 'kernel.reset') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->tagName = $tagName; } @@ -53,6 +57,10 @@ public function process(ContainerBuilder $container) $methods[$id] = []; } + if ('ignore' === ($attributes['on_invalid'] ?? null)) { + $attributes['method'] = '?'.$attributes['method']; + } + $methods[$id][] = $attributes['method']; } } diff --git a/DependencyInjection/ServicesResetter.php b/DependencyInjection/ServicesResetter.php index d9e0028ce1..0063deca36 100644 --- a/DependencyInjection/ServicesResetter.php +++ b/DependencyInjection/ServicesResetter.php @@ -26,6 +26,10 @@ class ServicesResetter implements ResetInterface private $resettableServices; private $resetMethods; + /** + * @param \Traversable $resettableServices + * @param array $resetMethods + */ public function __construct(\Traversable $resettableServices, array $resetMethods) { $this->resettableServices = $resettableServices; @@ -36,6 +40,10 @@ public function reset() { foreach ($this->resettableServices as $id => $service) { foreach ((array) $this->resetMethods[$id] as $resetMethod) { + if ('?' === $resetMethod[0] && !method_exists($service, $resetMethod = substr($resetMethod, 1))) { + continue; + } + $service->$resetMethod(); } } diff --git a/Event/KernelEvent.php b/Event/KernelEvent.php index 08558d533a..d9d425e114 100644 --- a/Event/KernelEvent.php +++ b/Event/KernelEvent.php @@ -28,7 +28,7 @@ class KernelEvent extends Event /** * @param int $requestType The request type the kernel is currently processing; one of - * HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST + * HttpKernelInterface::MAIN_REQUEST or HttpKernelInterface::SUB_REQUEST */ public function __construct(HttpKernelInterface $kernel, Request $request, ?int $requestType) { @@ -60,7 +60,7 @@ public function getRequest() /** * Returns the request type the kernel is currently processing. * - * @return int One of HttpKernelInterface::MASTER_REQUEST and + * @return int One of HttpKernelInterface::MAIN_REQUEST and * HttpKernelInterface::SUB_REQUEST */ public function getRequestType() @@ -68,13 +68,25 @@ public function getRequestType() return $this->requestType; } + /** + * Checks if this is the main request. + */ + public function isMainRequest(): bool + { + return HttpKernelInterface::MAIN_REQUEST === $this->requestType; + } + /** * Checks if this is a master request. * - * @return bool True if the request is a master request + * @return bool + * + * @deprecated since symfony/http-kernel 5.3, use isMainRequest() instead */ public function isMasterRequest() { - return HttpKernelInterface::MASTER_REQUEST === $this->requestType; + trigger_deprecation('symfony/http-kernel', '5.3', '"%s()" is deprecated, use "isMainRequest()" instead.', __METHOD__); + + return $this->isMainRequest(); } } diff --git a/Event/RequestEvent.php b/Event/RequestEvent.php index 0b2b98eeba..30ffcdcbde 100644 --- a/Event/RequestEvent.php +++ b/Event/RequestEvent.php @@ -49,7 +49,7 @@ public function setResponse(Response $response) /** * Returns whether a response was set. * - * @return bool Whether a response was set + * @return bool */ public function hasResponse() { diff --git a/Event/TerminateEvent.php b/Event/TerminateEvent.php index e0002fb56f..014ca535fe 100644 --- a/Event/TerminateEvent.php +++ b/Event/TerminateEvent.php @@ -18,8 +18,8 @@ /** * Allows to execute logic after a response was sent. * - * Since it's only triggered on master requests, the `getRequestType()` method - * will always return the value of `HttpKernelInterface::MASTER_REQUEST`. + * Since it's only triggered on main requests, the `getRequestType()` method + * will always return the value of `HttpKernelInterface::MAIN_REQUEST`. * * @author Jordi Boggiano */ @@ -29,7 +29,7 @@ final class TerminateEvent extends KernelEvent public function __construct(HttpKernelInterface $kernel, Request $request, Response $response) { - parent::__construct($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + parent::__construct($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->response = $response; } diff --git a/Event/ViewEvent.php b/Event/ViewEvent.php index 66a4ceab93..88211da417 100644 --- a/Event/ViewEvent.php +++ b/Event/ViewEvent.php @@ -42,7 +42,7 @@ public function __construct(HttpKernelInterface $kernel, Request $request, int $ /** * Returns the return value of the controller. * - * @return mixed The controller return value + * @return mixed */ public function getControllerResult() { diff --git a/EventListener/AbstractSessionListener.php b/EventListener/AbstractSessionListener.php index 1fe3264f7d..27749b24b2 100644 --- a/EventListener/AbstractSessionListener.php +++ b/EventListener/AbstractSessionListener.php @@ -13,13 +13,16 @@ use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Session\SessionUtils; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Contracts\Service\ResetInterface; /** * Sets the session onto the request on the "kernel.request" event and saves @@ -36,43 +39,63 @@ * * @internal */ -abstract class AbstractSessionListener implements EventSubscriberInterface +abstract class AbstractSessionListener implements EventSubscriberInterface, ResetInterface { - const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl'; + public const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl'; protected $container; private $sessionUsageStack = []; private $debug; - public function __construct(ContainerInterface $container = null, bool $debug = false) + /** + * @var array + */ + private $sessionOptions; + + public function __construct(ContainerInterface $container = null, bool $debug = false, array $sessionOptions = []) { $this->container = $container; $this->debug = $debug; + $this->sessionOptions = $sessionOptions; } public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } - $session = null; $request = $event->getRequest(); - if ($request->hasSession()) { - // no-op - } elseif (method_exists($request, 'setSessionFactory')) { - $request->setSessionFactory(function () { return $this->getSession(); }); - } elseif ($session = $this->getSession()) { - $request->setSession($session); + if (!$request->hasSession()) { + // This variable prevents calling `$this->getSession()` twice in case the Request (and the below factory) is cloned + $sess = null; + $request->setSessionFactory(function () use (&$sess, $request) { + if (!$sess) { + $sess = $this->getSession(); + + /* + * For supporting sessions in php runtime with runners like roadrunner or swoole, the session + * cookie needs to be read from the cookie bag and set on the session storage. + * + * Do not set it when a native php session is active. + */ + if ($sess && !$sess->isStarted() && \PHP_SESSION_ACTIVE !== session_status()) { + $sessionId = $sess->getId() ?: $request->cookies->get($sess->getName(), ''); + $sess->setId($sessionId); + } + } + + return $sess; + }); } - $session = $session ?? ($this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : null); + $session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : null; $this->sessionUsageStack[] = $session instanceof Session ? $session->getUsageIndex() : 0; } public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest() || (!$this->container->has('initialized_session') && !$event->getRequest()->hasSession())) { return; } @@ -81,7 +104,7 @@ public function onKernelResponse(ResponseEvent $event) // Always remove the internal header if present $response->headers->remove(self::NO_AUTO_CACHE_CONTROL_HEADER); - if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $event->getRequest()->getSession()) { + if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : ($event->getRequest()->hasSession() ? $event->getRequest()->getSession() : null)) { return; } @@ -112,6 +135,64 @@ public function onKernelResponse(ResponseEvent $event) * it is saved will just restart it. */ $session->save(); + + /* + * For supporting sessions in php runtime with runners like roadrunner or swoole the session + * cookie need to be written on the response object and should not be written by PHP itself. + */ + $sessionName = $session->getName(); + $sessionId = $session->getId(); + $sessionOptions = $this->getSessionOptions($this->sessionOptions); + $sessionCookiePath = $sessionOptions['cookie_path'] ?? '/'; + $sessionCookieDomain = $sessionOptions['cookie_domain'] ?? null; + $sessionCookieSecure = $sessionOptions['cookie_secure'] ?? false; + $sessionCookieHttpOnly = $sessionOptions['cookie_httponly'] ?? true; + $sessionCookieSameSite = $sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX; + $sessionUseCookies = $sessionOptions['use_cookies'] ?? true; + + SessionUtils::popSessionCookie($sessionName, $sessionId); + + if ($sessionUseCookies) { + $request = $event->getRequest(); + $requestSessionCookieId = $request->cookies->get($sessionName); + + $isSessionEmpty = $session->isEmpty() && empty($_SESSION); // checking $_SESSION to keep compatibility with native sessions + if ($requestSessionCookieId && $isSessionEmpty) { + // PHP internally sets the session cookie value to "deleted" when setcookie() is called with empty string $value argument + // which happens in \Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy + // when the session gets invalidated (for example on logout) so we must handle this case here too + // otherwise we would send two Set-Cookie headers back with the response + SessionUtils::popSessionCookie($sessionName, 'deleted'); + $response->headers->clearCookie( + $sessionName, + $sessionCookiePath, + $sessionCookieDomain, + $sessionCookieSecure, + $sessionCookieHttpOnly, + $sessionCookieSameSite + ); + } elseif ($sessionId !== $requestSessionCookieId && !$isSessionEmpty) { + $expire = 0; + $lifetime = $sessionOptions['cookie_lifetime'] ?? null; + if ($lifetime) { + $expire = time() + $lifetime; + } + + $response->headers->setCookie( + Cookie::create( + $sessionName, + $sessionId, + $expire, + $sessionCookiePath, + $sessionCookieDomain, + $sessionCookieSecure, + $sessionCookieHttpOnly, + false, + $sessionCookieSameSite + ) + ); + } + } } if ($session instanceof Session ? $session->getUsageIndex() === end($this->sessionUsageStack) : !$session->isStarted()) { @@ -119,10 +200,11 @@ public function onKernelResponse(ResponseEvent $event) } if ($autoCacheControl) { + $maxAge = $response->headers->hasCacheControlDirective('public') ? 0 : (int) $response->getMaxAge(); $response - ->setExpires(new \DateTime()) + ->setExpires(new \DateTimeImmutable('+'.$maxAge.' seconds')) ->setPrivate() - ->setMaxAge(0) + ->setMaxAge($maxAge) ->headers->addCacheControlDirective('must-revalidate'); } @@ -141,7 +223,7 @@ public function onKernelResponse(ResponseEvent $event) public function onFinishRequest(FinishRequestEvent $event) { - if ($event->isMasterRequest()) { + if ($event->isMainRequest()) { array_pop($this->sessionUsageStack); } } @@ -152,6 +234,10 @@ public function onSessionUsage(): void return; } + if ($this->container && $this->container->has('session_collector')) { + $this->container->get('session_collector')(); + } + if (!$requestStack = $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) { return; } @@ -187,10 +273,43 @@ public static function getSubscribedEvents(): array ]; } + public function reset(): void + { + if (\PHP_SESSION_ACTIVE === session_status()) { + session_abort(); + } + + session_unset(); + $_SESSION = []; + + if (!headers_sent()) { // session id can only be reset when no headers were so we check for headers_sent first + session_id(''); + } + } + /** * Gets the session object. * - * @return SessionInterface|null A SessionInterface instance or null if no session is available + * @return SessionInterface|null */ abstract protected function getSession(); + + private function getSessionOptions(array $sessionOptions): array + { + $mergedSessionOptions = []; + + foreach (session_get_cookie_params() as $key => $value) { + $mergedSessionOptions['cookie_'.$key] = $value; + } + + foreach ($sessionOptions as $key => $value) { + // do the same logic as in the NativeSessionStorage + if ('cookie_secure' === $key && 'auto' === $value) { + continue; + } + $mergedSessionOptions[$key] = $value; + } + + return $mergedSessionOptions; + } } diff --git a/EventListener/AbstractTestSessionListener.php b/EventListener/AbstractTestSessionListener.php index 19d13b8c46..838c2944b4 100644 --- a/EventListener/AbstractTestSessionListener.php +++ b/EventListener/AbstractTestSessionListener.php @@ -19,6 +19,8 @@ use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; +trigger_deprecation('symfony/http-kernel', '5.4', '"%s" is deprecated use "%s" instead.', AbstractTestSessionListener::class, AbstractSessionListener::class); + /** * TestSessionListener. * @@ -28,6 +30,8 @@ * @author Fabien Potencier * * @internal + * + * @deprecated since Symfony 5.4, use AbstractSessionListener instead */ abstract class AbstractTestSessionListener implements EventSubscriberInterface { @@ -41,12 +45,14 @@ public function __construct(array $sessionOptions = []) public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } // bootstrap the session - if (!$session = $this->getSession()) { + if ($event->getRequest()->hasSession()) { + $session = $event->getRequest()->getSession(); + } elseif (!$session = $this->getSession()) { return; } @@ -59,12 +65,12 @@ public function onKernelRequest(RequestEvent $event) } /** - * Checks if session was initialized and saves if current request is master + * Checks if session was initialized and saves if current request is the main request * Runs on 'kernel.response' in test environment. */ public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -81,7 +87,7 @@ public function onKernelResponse(ResponseEvent $event) if ($session instanceof Session ? !$session->isEmpty() || (null !== $this->sessionId && $session->getId() !== $this->sessionId) : $wasStarted) { $params = session_get_cookie_params() + ['samesite' => null]; foreach ($this->sessionOptions as $k => $v) { - if (0 === strpos($k, 'cookie_')) { + if (str_starts_with($k, 'cookie_')) { $params[substr($k, 7)] = $v; } } @@ -100,7 +106,7 @@ public function onKernelResponse(ResponseEvent $event) public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => ['onKernelRequest', 192], + KernelEvents::REQUEST => ['onKernelRequest', 127], // AFTER SessionListener KernelEvents::RESPONSE => ['onKernelResponse', -128], ]; } @@ -108,7 +114,7 @@ public static function getSubscribedEvents(): array /** * Gets the session object. * - * @return SessionInterface|null A SessionInterface instance or null if no session is available + * @return SessionInterface|null */ abstract protected function getSession(); } diff --git a/EventListener/DebugHandlersListener.php b/EventListener/DebugHandlersListener.php index 6b677edd7c..bd124f94d0 100644 --- a/EventListener/DebugHandlersListener.php +++ b/EventListener/DebugHandlersListener.php @@ -17,7 +17,6 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -27,36 +26,46 @@ * @author Nicolas Grekas * * @final + * + * @internal since Symfony 5.3 */ class DebugHandlersListener implements EventSubscriberInterface { + private $earlyHandler; private $exceptionHandler; private $logger; private $deprecationLogger; private $levels; private $throwAt; private $scream; - private $fileLinkFormat; private $scope; private $firstCall = true; private $hasTerminatedWithException; /** - * @param callable|null $exceptionHandler A handler that must support \Throwable instances that will be called on Exception - * @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants - * @param int|null $throwAt Thrown errors in a bit field of E_* constants, or null to keep the current value - * @param bool $scream Enables/disables screaming mode, where even silenced errors are logged - * @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files - * @param bool $scope Enables/disables scoping mode + * @param callable|null $exceptionHandler A handler that must support \Throwable instances that will be called on Exception + * @param array|int|null $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants + * @param int|null $throwAt Thrown errors in a bit field of E_* constants, or null to keep the current value + * @param bool $scream Enables/disables screaming mode, where even silenced errors are logged + * @param bool $scope Enables/disables scoping mode */ - public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = \E_ALL, ?int $throwAt = \E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, LoggerInterface $deprecationLogger = null) + public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = \E_ALL, ?int $throwAt = \E_ALL, bool $scream = true, $scope = true, $deprecationLogger = null, $fileLinkFormat = null) { + if (!\is_bool($scope)) { + trigger_deprecation('symfony/http-kernel', '5.4', 'Passing a $fileLinkFormat is deprecated.'); + $scope = $deprecationLogger; + $deprecationLogger = $fileLinkFormat; + } + + $handler = set_exception_handler('is_int'); + $this->earlyHandler = \is_array($handler) ? $handler[0] : null; + restore_exception_handler(); + $this->exceptionHandler = $exceptionHandler; $this->logger = $logger; - $this->levels = null === $levels ? \E_ALL : $levels; + $this->levels = $levels ?? \E_ALL; $this->throwAt = \is_int($throwAt) ? $throwAt : (null === $throwAt ? null : ($throwAt ? \E_ALL : null)); $this->scream = $scream; - $this->fileLinkFormat = $fileLinkFormat; $this->scope = $scope; $this->deprecationLogger = $deprecationLogger; } @@ -69,15 +78,19 @@ public function configure(object $event = null) if ($event instanceof ConsoleEvent && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { return; } - if (!$event instanceof KernelEvent ? !$this->firstCall : !$event->isMasterRequest()) { + if (!$event instanceof KernelEvent ? !$this->firstCall : !$event->isMainRequest()) { return; } $this->firstCall = $this->hasTerminatedWithException = false; - $handler = set_exception_handler('var_dump'); + $handler = set_exception_handler('is_int'); $handler = \is_array($handler) ? $handler[0] : null; restore_exception_handler(); + if (!$handler instanceof ErrorHandler) { + $handler = $this->earlyHandler; + } + if ($handler instanceof ErrorHandler) { if ($this->logger || $this->deprecationLogger) { $this->setDefaultLoggers($handler); diff --git a/EventListener/ErrorListener.php b/EventListener/ErrorListener.php index f5cac76bd8..b6fd0a357d 100644 --- a/EventListener/ErrorListener.php +++ b/EventListener/ErrorListener.php @@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -32,19 +33,49 @@ class ErrorListener implements EventSubscriberInterface protected $controller; protected $logger; protected $debug; + /** + * @var array|null}> + */ + protected $exceptionsMapping; - public function __construct($controller, LoggerInterface $logger = null, bool $debug = false) + /** + * @param array|null}> $exceptionsMapping + */ + public function __construct($controller, LoggerInterface $logger = null, bool $debug = false, array $exceptionsMapping = []) { $this->controller = $controller; $this->logger = $logger; $this->debug = $debug; + $this->exceptionsMapping = $exceptionsMapping; } public function logKernelException(ExceptionEvent $event) { - $e = FlattenException::createFromThrowable($event->getThrowable()); + $throwable = $event->getThrowable(); + $logLevel = null; + + foreach ($this->exceptionsMapping as $class => $config) { + if ($throwable instanceof $class && $config['log_level']) { + $logLevel = $config['log_level']; + break; + } + } + + foreach ($this->exceptionsMapping as $class => $config) { + if (!$throwable instanceof $class || !$config['status_code']) { + continue; + } + 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); + $event->setThrowable($throwable); + } + break; + } + + $e = FlattenException::createFromThrowable($throwable); - $this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine())); + $this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()), $logLevel); } public function onKernelException(ExceptionEvent $event) @@ -53,8 +84,8 @@ public function onKernelException(ExceptionEvent $event) return; } - $exception = $event->getThrowable(); - $request = $this->duplicateRequest($exception, $event->getRequest()); + $throwable = $event->getThrowable(); + $request = $this->duplicateRequest($throwable, $event->getRequest()); try { $response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false); @@ -65,14 +96,14 @@ public function onKernelException(ExceptionEvent $event) $prev = $e; do { - if ($exception === $wrapper = $prev) { + if ($throwable === $wrapper = $prev) { throw $e; } } while ($prev = $wrapper->getPrevious()); $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous'); $prev->setAccessible(true); - $prev->setValue($wrapper, $exception); + $prev->setValue($wrapper, $throwable); throw $e; } @@ -124,10 +155,12 @@ public static function getSubscribedEvents(): array /** * Logs an exception. */ - protected function logException(\Throwable $exception, string $message): void + protected function logException(\Throwable $exception, string $message, string $logLevel = null): void { if (null !== $this->logger) { - if (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) { + if (null !== $logLevel) { + $this->logger->log($logLevel, $message, ['exception' => $exception]); + } elseif (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) { $this->logger->critical($message, ['exception' => $exception]); } else { $this->logger->error($message, ['exception' => $exception]); diff --git a/EventListener/FragmentListener.php b/EventListener/FragmentListener.php index 14c6aa63d1..c01d9ad491 100644 --- a/EventListener/FragmentListener.php +++ b/EventListener/FragmentListener.php @@ -65,7 +65,7 @@ public function onKernelRequest(RequestEvent $event) return; } - if ($event->isMasterRequest()) { + if ($event->isMainRequest()) { $this->validateRequest($request); } diff --git a/EventListener/LocaleAwareListener.php b/EventListener/LocaleAwareListener.php index 62d03026a1..a126f06ecb 100644 --- a/EventListener/LocaleAwareListener.php +++ b/EventListener/LocaleAwareListener.php @@ -29,7 +29,7 @@ class LocaleAwareListener implements EventSubscriberInterface private $requestStack; /** - * @param LocaleAwareInterface[] $localeAwareServices + * @param iterable $localeAwareServices */ public function __construct(iterable $localeAwareServices, RequestStack $requestStack) { diff --git a/EventListener/LocaleListener.php b/EventListener/LocaleListener.php index 8037e32c0e..f19e13649e 100644 --- a/EventListener/LocaleListener.php +++ b/EventListener/LocaleListener.php @@ -32,12 +32,16 @@ class LocaleListener implements EventSubscriberInterface private $router; private $defaultLocale; private $requestStack; + private $useAcceptLanguageHeader; + private $enabledLocales; - public function __construct(RequestStack $requestStack, string $defaultLocale = 'en', RequestContextAwareInterface $router = null) + 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 setDefaultLocale(KernelEvent $event) @@ -64,6 +68,9 @@ private function setLocale(Request $request) { if ($locale = $request->attributes->get('_locale')) { $request->setLocale($locale); + } elseif ($this->useAcceptLanguageHeader && $this->enabledLocales && ($preferredLanguage = $request->getPreferredLanguage($this->enabledLocales))) { + $request->setLocale($preferredLanguage); + $request->attributes->set('_vary_by_language', true); } } diff --git a/EventListener/ProfilerListener.php b/EventListener/ProfilerListener.php index 3143f2b9a6..adbafe62e9 100644 --- a/EventListener/ProfilerListener.php +++ b/EventListener/ProfilerListener.php @@ -12,12 +12,15 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestMatcherInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; /** @@ -32,25 +35,29 @@ class ProfilerListener implements EventSubscriberInterface protected $profiler; protected $matcher; protected $onlyException; - protected $onlyMasterRequests; + protected $onlyMainRequests; protected $exception; + /** @var \SplObjectStorage */ protected $profiles; protected $requestStack; + protected $collectParameter; + /** @var \SplObjectStorage */ protected $parents; /** - * @param bool $onlyException True if the profiler only collects data when an exception occurs, false otherwise - * @param bool $onlyMasterRequests True if the profiler only collects data when the request is a master request, false otherwise + * @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 $onlyMasterRequests = false) + 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->onlyMasterRequests = $onlyMasterRequests; + $this->onlyMainRequests = $onlyMainRequests; $this->profiles = new \SplObjectStorage(); $this->parents = new \SplObjectStorage(); $this->requestStack = $requestStack; + $this->collectParameter = $collectParameter; } /** @@ -58,7 +65,7 @@ public function __construct(Profiler $profiler, RequestStack $requestStack, Requ */ public function onKernelException(ExceptionEvent $event) { - if ($this->onlyMasterRequests && !$event->isMasterRequest()) { + if ($this->onlyMainRequests && !$event->isMainRequest()) { return; } @@ -70,8 +77,7 @@ public function onKernelException(ExceptionEvent $event) */ public function onKernelResponse(ResponseEvent $event) { - $master = $event->isMasterRequest(); - if ($this->onlyMasterRequests && !$master) { + if ($this->onlyMainRequests && !$event->isMainRequest()) { return; } @@ -80,6 +86,10 @@ public function onKernelResponse(ResponseEvent $event) } $request = $event->getRequest(); + if (null !== $this->collectParameter && null !== $collectParameterValue = $request->get($this->collectParameter)) { + true === $collectParameterValue || filter_var($collectParameterValue, \FILTER_VALIDATE_BOOLEAN) ? $this->profiler->enable() : $this->profiler->disable(); + } + $exception = $this->exception; $this->exception = null; @@ -87,8 +97,21 @@ public function onKernelResponse(ResponseEvent $event) return; } - if (!$profile = $this->profiler->collect($request, $event->getResponse(), $exception)) { - return; + $session = $request->hasPreviousSession() && $request->hasSession() ? $request->getSession() : null; + + if ($session instanceof Session) { + $usageIndexValue = $usageIndexReference = &$session->getUsageIndex(); + $usageIndexReference = \PHP_INT_MIN; + } + + try { + if (!$profile = $this->profiler->collect($request, $event->getResponse(), $exception)) { + return; + } + } finally { + if ($session instanceof Session) { + $usageIndexReference = $usageIndexValue; + } } $this->profiles[$request] = $profile; diff --git a/EventListener/ResponseListener.php b/EventListener/ResponseListener.php index d8292aec43..a4090159bb 100644 --- a/EventListener/ResponseListener.php +++ b/EventListener/ResponseListener.php @@ -25,10 +25,12 @@ class ResponseListener implements EventSubscriberInterface { private $charset; + private $addContentLanguageHeader; - public function __construct(string $charset) + public function __construct(string $charset, bool $addContentLanguageHeader = false) { $this->charset = $charset; + $this->addContentLanguageHeader = $addContentLanguageHeader; } /** @@ -36,7 +38,7 @@ public function __construct(string $charset) */ public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -46,6 +48,14 @@ public function onKernelResponse(ResponseEvent $event) $response->setCharset($this->charset); } + if ($this->addContentLanguageHeader && !$response->isInformational() && !$response->isEmpty() && !$response->headers->has('Content-Language')) { + $response->headers->set('Content-Language', $event->getRequest()->getLocale()); + } + + if ($event->getRequest()->attributes->get('_vary_by_language')) { + $response->setVary('Accept-Language', false); + } + $response->prepare($event->getRequest()); } diff --git a/EventListener/RouterListener.php b/EventListener/RouterListener.php index 31bbc3b047..7c4da98928 100644 --- a/EventListener/RouterListener.php +++ b/EventListener/RouterListener.php @@ -50,9 +50,8 @@ class RouterListener implements EventSubscriberInterface private $debug; /** - * @param UrlMatcherInterface|RequestMatcherInterface $matcher The Url or Request matcher - * @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface) - * @param string $projectDir + * @param UrlMatcherInterface|RequestMatcherInterface $matcher The Url or Request matcher + * @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface) * * @throws \InvalidArgumentException */ @@ -67,7 +66,7 @@ public function __construct($matcher, RequestStack $requestStack, RequestContext } $this->matcher = $matcher; - $this->context = $context ?: $matcher->getContext(); + $this->context = $context ?? $matcher->getContext(); $this->requestStack = $requestStack; $this->logger = $logger; $this->projectDir = $projectDir; @@ -116,7 +115,7 @@ public function onKernelRequest(RequestEvent $event) if (null !== $this->logger) { $this->logger->info('Matched route "{route}".', [ - 'route' => isset($parameters['_route']) ? $parameters['_route'] : 'n/a', + 'route' => $parameters['_route'] ?? 'n/a', 'route_parameters' => $parameters, 'request_uri' => $request->getUri(), 'method' => $request->getMethod(), @@ -127,7 +126,7 @@ public function onKernelRequest(RequestEvent $event) 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->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); @@ -135,7 +134,7 @@ public function onKernelRequest(RequestEvent $event) throw new NotFoundHttpException($message, $e); } catch (MethodNotAllowedException $e) { - $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $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); } @@ -164,7 +163,7 @@ public static function getSubscribedEvents(): array private function createWelcomeResponse(): Response { $version = Kernel::VERSION; - $projectDir = realpath($this->projectDir).\DIRECTORY_SEPARATOR; + $projectDir = realpath((string) $this->projectDir).\DIRECTORY_SEPARATOR; $docVersion = substr(Kernel::VERSION, 0, 3); ob_start(); diff --git a/EventListener/SessionListener.php b/EventListener/SessionListener.php index e982a795b2..61887fde68 100644 --- a/EventListener/SessionListener.php +++ b/EventListener/SessionListener.php @@ -11,16 +11,16 @@ namespace Symfony\Component\HttpKernel\EventListener; -use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpKernel\Event\RequestEvent; /** * Sets the session in the request. * * When the passed container contains a "session_storage" entry which * holds a NativeSessionStorage instance, the "cookie_secure" option - * will be set to true whenever the current master request is secure. + * will be set to true whenever the current main request is secure. * * @author Fabien Potencier * @@ -28,25 +28,33 @@ */ class SessionListener extends AbstractSessionListener { - public function __construct(ContainerInterface $container, bool $debug = false) + public function onKernelRequest(RequestEvent $event) { - parent::__construct($container, $debug); - } + parent::onKernelRequest($event); - protected function getSession(): ?SessionInterface - { - if (!$this->container->has('session')) { - return null; + if (!$event->isMainRequest() || (!$this->container->has('session') && !$this->container->has('session_factory'))) { + return; } if ($this->container->has('session_storage') && ($storage = $this->container->get('session_storage')) instanceof NativeSessionStorage - && ($masterRequest = $this->container->get('request_stack')->getMasterRequest()) - && $masterRequest->isSecure() + && ($mainRequest = $this->container->get('request_stack')->getMainRequest()) + && $mainRequest->isSecure() ) { $storage->setOptions(['cookie_secure' => true]); } + } + + protected function getSession(): ?SessionInterface + { + if ($this->container->has('session')) { + return $this->container->get('session'); + } + + if ($this->container->has('session_factory')) { + return $this->container->get('session_factory')->createSession(); + } - return $this->container->get('session'); + return null; } } diff --git a/EventListener/StreamedResponseListener.php b/EventListener/StreamedResponseListener.php index 730ee5453f..b3f7ca40fa 100644 --- a/EventListener/StreamedResponseListener.php +++ b/EventListener/StreamedResponseListener.php @@ -31,7 +31,7 @@ class StreamedResponseListener implements EventSubscriberInterface */ public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } diff --git a/EventListener/SurrogateListener.php b/EventListener/SurrogateListener.php index 2ef4af7aa3..9081bff652 100644 --- a/EventListener/SurrogateListener.php +++ b/EventListener/SurrogateListener.php @@ -38,7 +38,7 @@ public function __construct(SurrogateInterface $surrogate = null) */ public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } diff --git a/EventListener/TestSessionListener.php b/EventListener/TestSessionListener.php index ff8b4aaa61..45fa312be7 100644 --- a/EventListener/TestSessionListener.php +++ b/EventListener/TestSessionListener.php @@ -14,12 +14,16 @@ use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; +trigger_deprecation('symfony/http-kernel', '5.4', '"%s" is deprecated, use "%s" instead.', TestSessionListener::class, SessionListener::class); + /** * Sets the session in the request. * * @author Fabien Potencier * * @final + * + * @deprecated since Symfony 5.4, use SessionListener instead */ class TestSessionListener extends AbstractTestSessionListener { @@ -33,10 +37,10 @@ public function __construct(ContainerInterface $container, array $sessionOptions protected function getSession(): ?SessionInterface { - if (!$this->container->has('session')) { - return null; + if ($this->container->has('session')) { + return $this->container->get('session'); } - return $this->container->get('session'); + return null; } } diff --git a/EventListener/ValidateRequestListener.php b/EventListener/ValidateRequestListener.php index 1f0c79822d..caa0f32aab 100644 --- a/EventListener/ValidateRequestListener.php +++ b/EventListener/ValidateRequestListener.php @@ -29,7 +29,7 @@ class ValidateRequestListener implements EventSubscriberInterface */ public function onKernelRequest(RequestEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); diff --git a/Exception/AccessDeniedHttpException.php b/Exception/AccessDeniedHttpException.php index 65e5f8c786..58680a3278 100644 --- a/Exception/AccessDeniedHttpException.php +++ b/Exception/AccessDeniedHttpException.php @@ -18,12 +18,18 @@ class AccessDeniedHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(403, $message, $previous, $headers, $code); } } diff --git a/Exception/BadRequestHttpException.php b/Exception/BadRequestHttpException.php index 7de91054b4..f530f7db49 100644 --- a/Exception/BadRequestHttpException.php +++ b/Exception/BadRequestHttpException.php @@ -17,12 +17,18 @@ class BadRequestHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(400, $message, $previous, $headers, $code); } } diff --git a/Exception/ConflictHttpException.php b/Exception/ConflictHttpException.php index ebb86ba6e9..79c36041c3 100644 --- a/Exception/ConflictHttpException.php +++ b/Exception/ConflictHttpException.php @@ -17,12 +17,18 @@ class ConflictHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(409, $message, $previous, $headers, $code); } } diff --git a/Exception/ControllerDoesNotReturnResponseException.php b/Exception/ControllerDoesNotReturnResponseException.php index 1e87690ff1..54c80be90f 100644 --- a/Exception/ControllerDoesNotReturnResponseException.php +++ b/Exception/ControllerDoesNotReturnResponseException.php @@ -38,7 +38,7 @@ public function __construct(string $message, callable $controller, string $file, private function parseControllerDefinition(callable $controller): ?array { - if (\is_string($controller) && false !== strpos($controller, '::')) { + if (\is_string($controller) && str_contains($controller, '::')) { $controller = explode('::', $controller); } diff --git a/Exception/GoneHttpException.php b/Exception/GoneHttpException.php index aea283a961..9ea65057b3 100644 --- a/Exception/GoneHttpException.php +++ b/Exception/GoneHttpException.php @@ -17,12 +17,18 @@ class GoneHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(410, $message, $previous, $headers, $code); } } diff --git a/Exception/HttpException.php b/Exception/HttpException.php index d822cd5d49..249fe366d5 100644 --- a/Exception/HttpException.php +++ b/Exception/HttpException.php @@ -21,8 +21,19 @@ class HttpException extends \RuntimeException implements HttpExceptionInterface private $statusCode; private $headers; - public function __construct(int $statusCode, string $message = null, \Throwable $previous = null, array $headers = [], ?int $code = 0) + public function __construct(int $statusCode, ?string $message = '', \Throwable $previous = null, array $headers = [], ?int $code = 0) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + if (null === $code) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + $this->statusCode = $statusCode; $this->headers = $headers; diff --git a/Exception/HttpExceptionInterface.php b/Exception/HttpExceptionInterface.php index 735e9c805e..4ae050945c 100644 --- a/Exception/HttpExceptionInterface.php +++ b/Exception/HttpExceptionInterface.php @@ -21,14 +21,14 @@ interface HttpExceptionInterface extends \Throwable /** * Returns the status code. * - * @return int An HTTP response status code + * @return int */ public function getStatusCode(); /** * Returns response headers. * - * @return array Response headers + * @return array */ public function getHeaders(); } diff --git a/Exception/InvalidMetadataException.php b/Exception/InvalidMetadataException.php new file mode 100644 index 0000000000..129267ab05 --- /dev/null +++ b/Exception/InvalidMetadataException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Exception; + +class InvalidMetadataException extends \LogicException +{ +} diff --git a/Exception/LengthRequiredHttpException.php b/Exception/LengthRequiredHttpException.php index 44fb770b60..fcac137852 100644 --- a/Exception/LengthRequiredHttpException.php +++ b/Exception/LengthRequiredHttpException.php @@ -17,12 +17,18 @@ class LengthRequiredHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(411, $message, $previous, $headers, $code); } } diff --git a/Exception/MethodNotAllowedHttpException.php b/Exception/MethodNotAllowedHttpException.php index c15e46ffc3..37576bcacb 100644 --- a/Exception/MethodNotAllowedHttpException.php +++ b/Exception/MethodNotAllowedHttpException.php @@ -17,13 +17,24 @@ class MethodNotAllowedHttpException extends HttpException { /** - * @param array $allow An array of allowed methods - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string[] $allow An array of allowed methods + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int|null $code The internal exception code */ - public function __construct(array $allow, string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + public function __construct(array $allow, ?string $message = '', \Throwable $previous = null, ?int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + if (null === $code) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + $headers['Allow'] = strtoupper(implode(', ', $allow)); parent::__construct(405, $message, $previous, $headers, $code); diff --git a/Exception/NotAcceptableHttpException.php b/Exception/NotAcceptableHttpException.php index c5f5324b1a..5a422406ba 100644 --- a/Exception/NotAcceptableHttpException.php +++ b/Exception/NotAcceptableHttpException.php @@ -17,12 +17,18 @@ class NotAcceptableHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(406, $message, $previous, $headers, $code); } } diff --git a/Exception/NotFoundHttpException.php b/Exception/NotFoundHttpException.php index 146b908a1e..a475113c5f 100644 --- a/Exception/NotFoundHttpException.php +++ b/Exception/NotFoundHttpException.php @@ -17,12 +17,18 @@ class NotFoundHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(404, $message, $previous, $headers, $code); } } diff --git a/Exception/PreconditionFailedHttpException.php b/Exception/PreconditionFailedHttpException.php index e878b10ad3..e23740a28d 100644 --- a/Exception/PreconditionFailedHttpException.php +++ b/Exception/PreconditionFailedHttpException.php @@ -17,12 +17,18 @@ class PreconditionFailedHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(412, $message, $previous, $headers, $code); } } diff --git a/Exception/PreconditionRequiredHttpException.php b/Exception/PreconditionRequiredHttpException.php index a6cb2f09a7..5c31fae822 100644 --- a/Exception/PreconditionRequiredHttpException.php +++ b/Exception/PreconditionRequiredHttpException.php @@ -19,12 +19,18 @@ class PreconditionRequiredHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(428, $message, $previous, $headers, $code); } } diff --git a/Exception/ServiceUnavailableHttpException.php b/Exception/ServiceUnavailableHttpException.php index c786ccf5f7..d5681bbeb3 100644 --- a/Exception/ServiceUnavailableHttpException.php +++ b/Exception/ServiceUnavailableHttpException.php @@ -17,13 +17,24 @@ class ServiceUnavailableHttpException extends HttpException { /** - * @param int|string $retryAfter The number of seconds or HTTP-date after which the request may be retried - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param int|string|null $retryAfter The number of seconds or HTTP-date after which the request may be retried + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int|null $code The internal exception code */ - public function __construct($retryAfter = null, string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + public function __construct($retryAfter = null, ?string $message = '', \Throwable $previous = null, ?int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + if (null === $code) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + if ($retryAfter) { $headers['Retry-After'] = $retryAfter; } diff --git a/Exception/TooManyRequestsHttpException.php b/Exception/TooManyRequestsHttpException.php index b709f1a2f1..fd74402b5d 100644 --- a/Exception/TooManyRequestsHttpException.php +++ b/Exception/TooManyRequestsHttpException.php @@ -19,13 +19,24 @@ class TooManyRequestsHttpException extends HttpException { /** - * @param int|string $retryAfter The number of seconds or HTTP-date after which the request may be retried - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param int|string|null $retryAfter The number of seconds or HTTP-date after which the request may be retried + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int|null $code The internal exception code */ - public function __construct($retryAfter = null, string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + public function __construct($retryAfter = null, ?string $message = '', \Throwable $previous = null, ?int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + if (null === $code) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + if ($retryAfter) { $headers['Retry-After'] = $retryAfter; } diff --git a/Exception/UnauthorizedHttpException.php b/Exception/UnauthorizedHttpException.php index fb86c1ea95..aeb9713a3d 100644 --- a/Exception/UnauthorizedHttpException.php +++ b/Exception/UnauthorizedHttpException.php @@ -17,13 +17,24 @@ class UnauthorizedHttpException extends HttpException { /** - * @param string $challenge WWW-Authenticate challenge string - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string $challenge WWW-Authenticate challenge string + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int|null $code The internal exception code */ - public function __construct(string $challenge, string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + public function __construct(string $challenge, ?string $message = '', \Throwable $previous = null, ?int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + if (null === $code) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $code to "%s()" is deprecated, pass 0 instead.', __METHOD__); + + $code = 0; + } + $headers['WWW-Authenticate'] = $challenge; parent::__construct(401, $message, $previous, $headers, $code); diff --git a/Exception/UnprocessableEntityHttpException.php b/Exception/UnprocessableEntityHttpException.php index 93d4bcef69..7b828b1d92 100644 --- a/Exception/UnprocessableEntityHttpException.php +++ b/Exception/UnprocessableEntityHttpException.php @@ -17,12 +17,18 @@ class UnprocessableEntityHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(422, $message, $previous, $headers, $code); } } diff --git a/Exception/UnsupportedMediaTypeHttpException.php b/Exception/UnsupportedMediaTypeHttpException.php index 7cda3a6202..7908423f42 100644 --- a/Exception/UnsupportedMediaTypeHttpException.php +++ b/Exception/UnsupportedMediaTypeHttpException.php @@ -17,12 +17,18 @@ class UnsupportedMediaTypeHttpException extends HttpException { /** - * @param string $message The internal exception message - * @param \Throwable $previous The previous exception - * @param int $code The internal exception code + * @param string|null $message The internal exception message + * @param \Throwable|null $previous The previous exception + * @param int $code The internal exception code */ - public function __construct(string $message = null, \Throwable $previous = null, int $code = 0, array $headers = []) + public function __construct(?string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []) { + if (null === $message) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Passing null as $message to "%s()" is deprecated, pass an empty string instead.', __METHOD__); + + $message = ''; + } + parent::__construct(415, $message, $previous, $headers, $code); } } diff --git a/Fragment/AbstractSurrogateFragmentRenderer.php b/Fragment/AbstractSurrogateFragmentRenderer.php index f81199d883..4e4d028b48 100644 --- a/Fragment/AbstractSurrogateFragmentRenderer.php +++ b/Fragment/AbstractSurrogateFragmentRenderer.php @@ -71,34 +71,29 @@ public function render($uri, Request $request, array $options = []) $uri = $this->generateSignedFragmentUri($uri, $request); } - $alt = isset($options['alt']) ? $options['alt'] : null; + $alt = $options['alt'] ?? null; if ($alt instanceof ControllerReference) { $alt = $this->generateSignedFragmentUri($alt, $request); } - $tag = $this->surrogate->renderIncludeTag($uri, $alt, isset($options['ignore_errors']) ? $options['ignore_errors'] : false, isset($options['comment']) ? $options['comment'] : ''); + $tag = $this->surrogate->renderIncludeTag($uri, $alt, $options['ignore_errors'] ?? false, $options['comment'] ?? ''); return new Response($tag); } private function generateSignedFragmentUri(ControllerReference $uri, Request $request): string { - if (null === $this->signer) { - throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.'); - } - - // we need to sign the absolute URI, but want to return the path only. - $fragmentUri = $this->signer->sign($this->generateFragmentUri($uri, $request, true)); - - return substr($fragmentUri, \strlen($request->getSchemeAndHttpHost())); + return (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request); } private function containsNonScalars(array $values): bool { foreach ($values as $value) { - if (\is_array($value)) { - return $this->containsNonScalars($value); - } elseif (!is_scalar($value) && null !== $value) { + if (\is_scalar($value) || null === $value) { + continue; + } + + if (!\is_array($value) || $this->containsNonScalars($value)) { return true; } } diff --git a/Fragment/FragmentHandler.php b/Fragment/FragmentHandler.php index f51978ac7e..1ecaaef1aa 100644 --- a/Fragment/FragmentHandler.php +++ b/Fragment/FragmentHandler.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\Exception\HttpException; /** * Renders a URI that represents a resource fragment. @@ -62,10 +63,10 @@ public function addRenderer(FragmentRendererInterface $renderer) * * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance * - * @return string|null The Response content or null when the Response is streamed + * @return string|null * * @throws \InvalidArgumentException when the renderer does not exist - * @throws \LogicException when no master request is being handled + * @throws \LogicException when no main request is being handled */ public function render($uri, string $renderer = 'inline', array $options = []) { @@ -97,7 +98,8 @@ public function render($uri, string $renderer = 'inline', array $options = []) protected function deliver(Response $response) { if (!$response->isSuccessful()) { - throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %d).', $this->requestStack->getCurrentRequest()->getUri(), $response->getStatusCode())); + $responseStatusCode = $response->getStatusCode(); + 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/FragmentRendererInterface.php b/Fragment/FragmentRendererInterface.php index 4f8ac50b16..568b1781a9 100644 --- a/Fragment/FragmentRendererInterface.php +++ b/Fragment/FragmentRendererInterface.php @@ -27,14 +27,14 @@ interface FragmentRendererInterface * * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance * - * @return Response A Response instance + * @return Response */ public function render($uri, Request $request, array $options = []); /** * Gets the name of the strategy. * - * @return string The strategy name + * @return string */ public function getName(); } diff --git a/Fragment/FragmentUriGenerator.php b/Fragment/FragmentUriGenerator.php new file mode 100644 index 0000000000..4c0fac997a --- /dev/null +++ b/Fragment/FragmentUriGenerator.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Fragment; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\UriSigner; + +/** + * Generates a fragment URI. + * + * @author Kévin Dunglas + * @author Fabien Potencier + */ +final class FragmentUriGenerator implements FragmentUriGeneratorInterface +{ + private $fragmentPath; + private $signer; + private $requestStack; + + public function __construct(string $fragmentPath, UriSigner $signer = null, RequestStack $requestStack = null) + { + $this->fragmentPath = $fragmentPath; + $this->signer = $signer; + $this->requestStack = $requestStack; + } + + /** + * {@inheritDoc} + */ + public function generate(ControllerReference $controller, Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string + { + if (null === $request && (null === $this->requestStack || null === $request = $this->requestStack->getCurrentRequest())) { + throw new \LogicException('Generating a fragment URL can only be done when handling a Request.'); + } + + if ($sign && null === $this->signer) { + throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.'); + } + + if ($strict) { + $this->checkNonScalar($controller->attributes); + } + + // We need to forward the current _format and _locale values as we don't have + // a proper routing pattern to do the job for us. + // This makes things inconsistent if you switch from rendering a controller + // to rendering a route if the route pattern does not contain the special + // _format and _locale placeholders. + if (!isset($controller->attributes['_format'])) { + $controller->attributes['_format'] = $request->getRequestFormat(); + } + if (!isset($controller->attributes['_locale'])) { + $controller->attributes['_locale'] = $request->getLocale(); + } + + $controller->attributes['_controller'] = $controller->controller; + $controller->query['_path'] = http_build_query($controller->attributes, '', '&'); + $path = $this->fragmentPath.'?'.http_build_query($controller->query, '', '&'); + + // we need to sign the absolute URI, but want to return the path only. + $fragmentUri = $sign || $absolute ? $request->getUriForPath($path) : $request->getBaseUrl().$path; + + if (!$sign) { + return $fragmentUri; + } + + $fragmentUri = $this->signer->sign($fragmentUri); + + return $absolute ? $fragmentUri : substr($fragmentUri, \strlen($request->getSchemeAndHttpHost())); + } + + private function checkNonScalar(array $values): void + { + foreach ($values as $key => $value) { + 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)); + } + } + } +} diff --git a/Fragment/FragmentUriGeneratorInterface.php b/Fragment/FragmentUriGeneratorInterface.php new file mode 100644 index 0000000000..b211f5e373 --- /dev/null +++ b/Fragment/FragmentUriGeneratorInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Fragment; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ControllerReference; + +/** + * Interface implemented by rendering strategies able to generate an URL for a fragment. + * + * @author Kévin Dunglas + */ +interface FragmentUriGeneratorInterface +{ + /** + * Generates a fragment URI for a given controller. + * + * @param bool $absolute Whether to generate an absolute URL or not + * @param bool $strict Whether to allow non-scalar attributes or not + * @param bool $sign Whether to sign the URL or not + */ + public function generate(ControllerReference $controller, Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string; +} diff --git a/Fragment/HIncludeFragmentRenderer.php b/Fragment/HIncludeFragmentRenderer.php index 634eabfba5..446ce2d9df 100644 --- a/Fragment/HIncludeFragmentRenderer.php +++ b/Fragment/HIncludeFragmentRenderer.php @@ -43,7 +43,7 @@ public function __construct(Environment $twig = null, UriSigner $signer = null, /** * Checks if a templating engine has been set. * - * @return bool true if the templating engine has been set, false otherwise + * @return bool */ public function hasTemplating() { @@ -62,18 +62,13 @@ public function hasTemplating() public function render($uri, Request $request, array $options = []) { if ($uri instanceof ControllerReference) { - if (null === $this->signer) { - throw new \LogicException('You must use a proper URI when using the Hinclude rendering strategy or set a URL signer.'); - } - - // we need to sign the absolute URI, but want to return the path only. - $uri = substr($this->signer->sign($this->generateFragmentUri($uri, $request, true)), \strlen($request->getSchemeAndHttpHost())); + $uri = (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request); } // We need to replace ampersands in the URI with the encoded form in order to return valid html/xml content. $uri = str_replace('&', '&', $uri); - $template = isset($options['default']) ? $options['default'] : $this->globalDefaultTemplate; + $template = $options['default'] ?? $this->globalDefaultTemplate; if (null !== $this->twig && $template && $this->twig->getLoader()->exists($template)) { $content = $this->twig->render($template); } else { diff --git a/Fragment/InlineFragmentRenderer.php b/Fragment/InlineFragmentRenderer.php index 3bbdbd3ce8..ea45fdcb3f 100644 --- a/Fragment/InlineFragmentRenderer.php +++ b/Fragment/InlineFragmentRenderer.php @@ -105,7 +105,7 @@ public function render($uri, Request $request, array $options = []) } } - protected function createSubRequest($uri, Request $request) + protected function createSubRequest(string $uri, Request $request) { $cookies = $request->cookies->all(); $server = $request->server->all(); diff --git a/Fragment/RoutableFragmentRenderer.php b/Fragment/RoutableFragmentRenderer.php index c9a175758d..e922ffb64d 100644 --- a/Fragment/RoutableFragmentRenderer.php +++ b/Fragment/RoutableFragmentRenderer.php @@ -22,7 +22,10 @@ */ abstract class RoutableFragmentRenderer implements FragmentRendererInterface { - private $fragmentPath = '/_fragment'; + /** + * @internal + */ + protected $fragmentPath = '/_fragment'; /** * Sets the fragment path that triggers the fragment listener. @@ -40,47 +43,10 @@ public function setFragmentPath(string $path) * @param bool $absolute Whether to generate an absolute URL or not * @param bool $strict Whether to allow non-scalar attributes or not * - * @return string A fragment URI + * @return string */ protected function generateFragmentUri(ControllerReference $reference, Request $request, bool $absolute = false, bool $strict = true) { - if ($strict) { - $this->checkNonScalar($reference->attributes); - } - - // We need to forward the current _format and _locale values as we don't have - // a proper routing pattern to do the job for us. - // This makes things inconsistent if you switch from rendering a controller - // to rendering a route if the route pattern does not contain the special - // _format and _locale placeholders. - if (!isset($reference->attributes['_format'])) { - $reference->attributes['_format'] = $request->getRequestFormat(); - } - if (!isset($reference->attributes['_locale'])) { - $reference->attributes['_locale'] = $request->getLocale(); - } - - $reference->attributes['_controller'] = $reference->controller; - - $reference->query['_path'] = http_build_query($reference->attributes, '', '&'); - - $path = $this->fragmentPath.'?'.http_build_query($reference->query, '', '&'); - - if ($absolute) { - return $request->getUriForPath($path); - } - - return $request->getBaseUrl().$path; - } - - private function checkNonScalar(array $values) - { - foreach ($values as $key => $value) { - 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)); - } - } + return (new FragmentUriGenerator($this->fragmentPath))->generate($reference, $request, $absolute, $strict, false); } } diff --git a/HttpCache/AbstractSurrogate.php b/HttpCache/AbstractSurrogate.php index bcfa13446a..f2d809e8de 100644 --- a/HttpCache/AbstractSurrogate.php +++ b/HttpCache/AbstractSurrogate.php @@ -41,7 +41,7 @@ public function __construct(array $contentTypes = ['text/html', 'text/xml', 'app /** * Returns a new cache strategy instance. * - * @return ResponseCacheStrategyInterface A ResponseCacheStrategyInterface instance + * @return ResponseCacheStrategyInterface */ public function createCacheStrategy() { @@ -57,7 +57,7 @@ public function hasSurrogateCapability(Request $request) return false; } - return false !== strpos($value, sprintf('%s/1.0', strtoupper($this->getName()))); + return str_contains($value, sprintf('%s/1.0', strtoupper($this->getName()))); } /** @@ -95,7 +95,7 @@ public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreE try { $response = $cache->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); - if (!$response->isSuccessful()) { + 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())); } diff --git a/HttpCache/Esi.php b/HttpCache/Esi.php index 458b2c5d74..cd6a00a10d 100644 --- a/HttpCache/Esi.php +++ b/HttpCache/Esi.php @@ -37,7 +37,7 @@ public function getName() */ public function addSurrogateControl(Response $response) { - if (false !== strpos($response->getContent(), 'getContent(), 'headers->set('Surrogate-Control', 'content="ESI/1.0"'); } } @@ -97,7 +97,7 @@ public function process(Request $request, Response $response) $chunks[$i] = sprintf('surrogate->handle($this, %s, %s, %s) ?>'."\n", var_export($options['src'], true), - var_export(isset($options['alt']) ? $options['alt'] : '', true), + var_export($options['alt'] ?? '', true), isset($options['onerror']) && 'continue' === $options['onerror'] ? 'true' : 'false' ); ++$i; diff --git a/HttpCache/HttpCache.php b/HttpCache/HttpCache.php index f8f5a9ad78..28be364c17 100644 --- a/HttpCache/HttpCache.php +++ b/HttpCache/HttpCache.php @@ -5,12 +5,14 @@ * * (c) Fabien Potencier * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/* * This code is partially based on the Rack-Cache library by Ryan Tomayko, * which is released under the MIT license. * (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801) - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; @@ -44,7 +46,7 @@ class HttpCache implements HttpKernelInterface, TerminableInterface * will try to carry on and deliver a meaningful response. * * * trace_level May be one of 'none', 'short' and 'full'. For 'short', a concise trace of the - * master request will be added as an HTTP header. 'full' will add traces for all + * main request will be added as an HTTP header. 'full' will add traces for all * requests (including ESI subrequests). (default: 'full' if in debug; 'none' otherwise) * * * trace_header Header name to use for traces. (default: X-Symfony-Cache) @@ -106,7 +108,7 @@ public function __construct(HttpKernelInterface $kernel, StoreInterface $store, /** * Gets the current store. * - * @return StoreInterface A StoreInterface instance + * @return StoreInterface */ public function getStore() { @@ -116,7 +118,7 @@ public function getStore() /** * Returns an array of events that took place during processing of the last request. * - * @return array An array of events + * @return array */ public function getTraces() { @@ -143,7 +145,7 @@ private function addTraces(Response $response) /** * Returns a log message for the events of the last request processing. * - * @return string A log message + * @return string */ public function getLog() { @@ -156,9 +158,9 @@ public function getLog() } /** - * Gets the Request instance associated with the master request. + * Gets the Request instance associated with the main request. * - * @return Request A Request instance + * @return Request */ public function getRequest() { @@ -168,7 +170,7 @@ public function getRequest() /** * Gets the Kernel instance. * - * @return HttpKernelInterface An HttpKernelInterface instance + * @return HttpKernelInterface */ public function getKernel() { @@ -178,7 +180,7 @@ public function getKernel() /** * Gets the Surrogate instance. * - * @return SurrogateInterface A Surrogate instance + * @return SurrogateInterface * * @throws \LogicException */ @@ -190,10 +192,10 @@ public function getSurrogate() /** * {@inheritdoc} */ - public function handle(Request $request, int $type = HttpKernelInterface::MASTER_REQUEST, bool $catch = true) + public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true) { // FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism - if (HttpKernelInterface::MASTER_REQUEST === $type) { + if (HttpKernelInterface::MAIN_REQUEST === $type) { $this->traces = []; // Keep a clone of the original request for surrogates so they can access it. // We must clone here to get a separate instance because the application will modify the request during @@ -224,12 +226,12 @@ public function handle(Request $request, int $type = HttpKernelInterface::MASTER $this->restoreResponseBody($request, $response); - if (HttpKernelInterface::MASTER_REQUEST === $type) { + if (HttpKernelInterface::MAIN_REQUEST === $type) { $this->addTraces($response); } if (null !== $this->surrogate) { - if (HttpKernelInterface::MASTER_REQUEST === $type) { + if (HttpKernelInterface::MAIN_REQUEST === $type) { $this->surrogateCacheStrategy->update($response); } else { $this->surrogateCacheStrategy->add($response); @@ -258,7 +260,7 @@ public function terminate(Request $request, Response $response) * * @param bool $catch Whether to process exceptions * - * @return Response A Response instance + * @return Response */ protected function pass(Request $request, bool $catch = false) { @@ -272,7 +274,7 @@ protected function pass(Request $request, bool $catch = false) * * @param bool $catch Whether to process exceptions * - * @return Response A Response instance + * @return Response * * @throws \Exception * @@ -320,7 +322,7 @@ protected function invalidate(Request $request, bool $catch = false) * * @param bool $catch Whether to process exceptions * - * @return Response A Response instance + * @return Response * * @throws \Exception */ @@ -369,7 +371,7 @@ protected function lookup(Request $request, bool $catch = false) * * @param bool $catch Whether to process exceptions * - * @return Response A Response instance + * @return Response */ protected function validate(Request $request, Response $entry, bool $catch = false) { @@ -382,7 +384,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal // add our cached last-modified validator if ($entry->headers->has('Last-Modified')) { - $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified')); + $subRequest->headers->set('If-Modified-Since', $entry->headers->get('Last-Modified')); } // Add our cached etag validator to the environment. @@ -391,7 +393,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal $cachedEtags = $entry->getEtag() ? [$entry->getEtag()] : []; $requestEtags = $request->getETags(); if ($etags = array_unique(array_merge($cachedEtags, $requestEtags))) { - $subRequest->headers->set('if_none_match', implode(', ', $etags)); + $subRequest->headers->set('If-None-Match', implode(', ', $etags)); } $response = $this->forward($subRequest, $catch, $entry); @@ -432,7 +434,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal * * @param bool $catch Whether to process exceptions * - * @return Response A Response instance + * @return Response */ protected function fetch(Request $request, bool $catch = false) { @@ -444,8 +446,8 @@ protected function fetch(Request $request, bool $catch = false) } // avoid that the backend sends no content - $subRequest->headers->remove('if_modified_since'); - $subRequest->headers->remove('if_none_match'); + $subRequest->headers->remove('If-Modified-Since'); + $subRequest->headers->remove('If-None-Match'); $response = $this->forward($subRequest, $catch); @@ -465,7 +467,7 @@ protected function fetch(Request $request, bool $catch = false) * @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 A Response instance + * @return Response */ protected function forward(Request $request, bool $catch = false, Response $entry = null) { @@ -474,7 +476,7 @@ protected function forward(Request $request, bool $catch = false, Response $entr } // always a "master" request (as the real master request can be in cache) - $response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch); + $response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $catch); /* * Support stale-if-error given on Responses or as a config option. @@ -538,7 +540,7 @@ protected function forward(Request $request, bool $catch = false, Response $entr /** * Checks whether the cache entry is "fresh enough" to satisfy the Request. * - * @return bool true if the cache entry if fresh enough, false otherwise + * @return bool */ protected function isFreshEnough(Request $request, Response $entry) { @@ -716,7 +718,7 @@ private function mayServeStaleWhileRevalidate(Response $entry): bool $timeout = $this->options['stale_while_revalidate']; } - return abs($entry->getTtl()) < $timeout; + return abs($entry->getTtl() ?? 0) < $timeout; } /** diff --git a/HttpCache/ResponseCacheStrategy.php b/HttpCache/ResponseCacheStrategy.php index c30fface60..cf8682257e 100644 --- a/HttpCache/ResponseCacheStrategy.php +++ b/HttpCache/ResponseCacheStrategy.php @@ -17,7 +17,7 @@ * ResponseCacheStrategy knows how to compute the Response cache HTTP header * based on the different response cache headers. * - * This implementation changes the master response TTL to the smallest TTL received + * This implementation changes the main response TTL to the smallest TTL received * or force validation if one of the surrogates has validation cache strategy. * * @author Fabien Potencier @@ -27,12 +27,12 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface /** * Cache-Control headers that are sent to the final response if they appear in ANY of the responses. */ - private static $overrideDirectives = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate']; + private const OVERRIDE_DIRECTIVES = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate']; /** * Cache-Control headers that are sent to the final response if they appear in ALL of the responses. */ - private static $inheritDirectives = ['public', 'immutable']; + private const INHERIT_DIRECTIVES = ['public', 'immutable']; private $embeddedResponses = 0; private $isNotCacheableResponseEmbedded = false; @@ -60,13 +60,13 @@ public function add(Response $response) { ++$this->embeddedResponses; - foreach (self::$overrideDirectives as $directive) { + foreach (self::OVERRIDE_DIRECTIVES as $directive) { if ($response->headers->hasCacheControlDirective($directive)) { $this->flagDirectives[$directive] = true; } } - foreach (self::$inheritDirectives as $directive) { + foreach (self::INHERIT_DIRECTIVES as $directive) { if (false !== $this->flagDirectives[$directive]) { $this->flagDirectives[$directive] = $response->headers->hasCacheControlDirective($directive); } @@ -81,12 +81,15 @@ public function add(Response $response) return; } - $this->storeRelativeAgeDirective('max-age', $response->headers->getCacheControlDirective('max-age'), $age); - $this->storeRelativeAgeDirective('s-maxage', $response->headers->getCacheControlDirective('s-maxage') ?: $response->headers->getCacheControlDirective('max-age'), $age); + $isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public'); + $maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null; + $this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable); + $sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge; + $this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable); $expires = $response->getExpires(); $expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null; - $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0); + $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable); } /** @@ -197,11 +200,29 @@ private function willMakeFinalResponseUncacheable(Response $response): bool * we have to subtract the age so that the value is normalized for an age of 0. * * If the value is lower than the currently stored value, we update the value, to keep a rolling - * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response. + * minimal value of each instruction. + * + * If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will + * not be set on the final response. In this case, not all responses had the directive set and no + * value can be found that satisfies the requirements of all responses. The directive will be dropped + * from the final response. + * + * If the isHeuristicallyCacheable parameter is true, however, the current response has been marked + * as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve + * as an upper bound. In this case, we can proceed and possibly keep the directive on the final response. */ - private function storeRelativeAgeDirective(string $directive, ?int $value, int $age) + private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable) { if (null === $value) { + if ($isHeuristicallyCacheable) { + /* + * See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2 + * This particular response does not require maximum lifetime; heuristics might be applied. + * Other responses, however, might have more stringent requirements on maximum lifetime. + * So, return early here so that the final response can have the more limiting value set. + */ + return; + } $this->ageDirectives[$directive] = false; } diff --git a/HttpCache/Ssi.php b/HttpCache/Ssi.php index 0ba351dd12..f114e05cfb 100644 --- a/HttpCache/Ssi.php +++ b/HttpCache/Ssi.php @@ -34,7 +34,7 @@ public function getName() */ public function addSurrogateControl(Response $response) { - if (false !== strpos($response->getContent(), '