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 f89b1cd7a0..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,16 +47,14 @@ 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 = []; $previousHandler = set_error_handler(function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { - if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { + if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; } @@ -63,7 +64,7 @@ public function warmUp(string $cacheDir) return null; } - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 3); // Clean the trace by removing first frames added by the error handler itself. for ($i = 0; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { @@ -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 0632f92a18..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_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 34c6dc4297..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,12 +224,12 @@ private function getContainerDeprecationLogs(): array private function getContainerCompilerLogs(string $compilerLogsFilepath = null): array { - if (!file_exists($compilerLogsFilepath)) { + if (!is_file($compilerLogsFilepath)) { return []; } $logs = []; - foreach (file($compilerLogsFilepath, FILE_IGNORE_NEW_LINES) as $log) { + foreach (file($compilerLogsFilepath, \FILE_IGNORE_NEW_LINES) as $log) { $log = explode(': ', $log, 2); if (!isset($log[1]) || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $log[0])) { $log = ['Unknown Compiler Pass', implode(': ', $log)]; @@ -226,7 +302,7 @@ private function isSilencedOrDeprecationErrorLog(array $log): bool return true; } - if ($exception instanceof \ErrorException && \in_array($exception->getSeverity(), [E_DEPRECATED, E_USER_DEPRECATED], true)) { + if ($exception instanceof \ErrorException && \in_array($exception->getSeverity(), [\E_DEPRECATED, \E_USER_DEPRECATED], true)) { return true; } 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 b92518d806..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 = []; @@ -64,8 +69,8 @@ public function collect(Request $request, Response $response, \Throwable $except if ($request->hasSession()) { $session = $request->getSession(); if ($session->isStarted()) { - $sessionMetadata['Created'] = date(DATE_RFC822, $session->getMetadataBag()->getCreated()); - $sessionMetadata['Last used'] = date(DATE_RFC822, $session->getMetadataBag()->getLastUsed()); + $sessionMetadata['Created'] = date(\DATE_RFC822, $session->getMetadataBag()->getCreated()); + $sessionMetadata['Last used'] = date(\DATE_RFC822, $session->getMetadataBag()->getLastUsed()); $sessionMetadata['Lifetime'] = $session->getMetadataBag()->getLifetime(); $sessionAttributes = $session->all(); $flashes = $session->getFlashBag()->peekAll(); @@ -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(); @@ -261,7 +282,7 @@ public function getPrettyJson() { $decoded = json_decode($this->getContent()); - return JSON_ERROR_NONE === json_last_error() ? json_encode($decoded, JSON_PRETTY_PRINT) : null; + return \JSON_ERROR_NONE === json_last_error() ? json_encode($decoded, \JSON_PRETTY_PRINT) : null; } public function getContentType() @@ -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 74abb81862..9ac688cc56 100644 --- a/Debug/FileLinkFormatter.php +++ b/Debug/FileLinkFormatter.php @@ -24,20 +24,30 @@ */ 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); + $fileLinkFormat = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, \PREG_SPLIT_DELIM_CAPTURE); } $this->fileLinkFormat = $fileLinkFormat; @@ -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 82d45a8dcf..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; @@ -100,13 +107,13 @@ public function process(ContainerBuilder $container) } foreach (['action', 'argument', 'id'] as $k) { if (!isset($attributes[$k][0])) { - throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "%s" %s for service "%s".', $k, $this->controllerTag, json_encode($attributes, JSON_UNESCAPED_UNICODE), $id)); + throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "%s" %s for service "%s".', $k, $this->controllerTag, json_encode($attributes, \JSON_UNESCAPED_UNICODE), $id)); } } 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 44b1ddb187..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->throwAt = \is_int($throwAt) ? $throwAt : (null === $throwAt ? null : ($throwAt ? E_ALL : null)); + $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); @@ -94,7 +107,7 @@ public function configure(object $event = null) $handler->screamAt($levels); } if ($this->scope) { - $handler->scopeAt($levels & ~E_USER_DEPRECATED & ~E_DEPRECATED); + $handler->scopeAt($levels & ~\E_USER_DEPRECATED & ~\E_DEPRECATED); } else { $handler->scopeAt(0, true); } @@ -142,15 +155,15 @@ private function setDefaultLoggers(ErrorHandler $handler): void $levelsDeprecatedOnly = []; $levelsWithoutDeprecated = []; foreach ($this->levels as $type => $log) { - if (E_DEPRECATED == $type || E_USER_DEPRECATED == $type) { + if (\E_DEPRECATED == $type || \E_USER_DEPRECATED == $type) { $levelsDeprecatedOnly[$type] = $log; } else { $levelsWithoutDeprecated[$type] = $log; } } } else { - $levelsDeprecatedOnly = $this->levels & (E_DEPRECATED | E_USER_DEPRECATED); - $levelsWithoutDeprecated = $this->levels & ~E_DEPRECATED & ~E_USER_DEPRECATED; + $levelsDeprecatedOnly = $this->levels & (\E_DEPRECATED | \E_USER_DEPRECATED); + $levelsWithoutDeprecated = $this->levels & ~\E_DEPRECATED & ~\E_USER_DEPRECATED; } $defaultLoggerLevels = $this->levels; 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 618859d0f9..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 { @@ -86,7 +81,7 @@ public function render($uri, Request $request, array $options = []) } $renderedAttributes = ''; if (\count($attributes) > 0) { - $flags = ENT_QUOTES | ENT_SUBSTITUTE; + $flags = \ENT_QUOTES | \ENT_SUBSTITUTE; foreach ($attributes as $attribute => $value) { $renderedAttributes .= sprintf( ' %s="%s"', 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 af67e8ffb8..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"'); } } @@ -80,13 +80,13 @@ public function process(Request $request, Response $response) $content = preg_replace('#.*?#s', '', $content); $content = preg_replace('#]+>#s', '', $content); - $chunks = preg_split('##', $content, -1, PREG_SPLIT_DELIM_CAPTURE); + $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); $i = 1; while (isset($chunks[$i])) { $options = []; - preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, PREG_SET_ORDER); + preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER); foreach ($matches as $set) { $options[$set[1]] = $set[2]; } @@ -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 587c43a0eb..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(), '#', $content, -1, PREG_SPLIT_DELIM_CAPTURE); + $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); $i = 1; while (isset($chunks[$i])) { $options = []; - preg_match_all('/(virtual)="([^"]*?)"/', $chunks[$i], $matches, PREG_SET_ORDER); + preg_match_all('/(virtual)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER); foreach ($matches as $set) { $options[$set[1]] = $set[2]; } diff --git a/HttpCache/Store.php b/HttpCache/Store.php index 22a03f261f..8087e0cb18 100644 --- a/HttpCache/Store.php +++ b/HttpCache/Store.php @@ -25,20 +25,32 @@ class Store implements StoreInterface { protected $root; + /** @var \SplObjectStorage */ private $keyCache; - private $locks; + /** @var array */ + private $locks = []; + private $options; /** + * Constructor. + * + * The available options are: + * + * * private_headers Set of response headers that should not be stored + * when a response is cached. (default: Set-Cookie) + * * @throws \RuntimeException */ - public function __construct(string $root) + public function __construct(string $root, array $options = []) { $this->root = $root; - if (!file_exists($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) { + if (!is_dir($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) { throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root)); } $this->keyCache = new \SplObjectStorage(); - $this->locks = []; + $this->options = array_merge([ + 'private_headers' => ['Set-Cookie'], + ], $options); } /** @@ -48,7 +60,7 @@ public function cleanup() { // unlock everything foreach ($this->locks as $lock) { - flock($lock, LOCK_UN); + flock($lock, \LOCK_UN); fclose($lock); } @@ -66,11 +78,11 @@ public function lock(Request $request) if (!isset($this->locks[$key])) { $path = $this->getPath($key); - if (!file_exists(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) { + if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) { return $path; } - $h = fopen($path, 'cb'); - if (!flock($h, LOCK_EX | LOCK_NB)) { + $h = fopen($path, 'c'); + if (!flock($h, \LOCK_EX | \LOCK_NB)) { fclose($h); return $path; @@ -92,7 +104,7 @@ public function unlock(Request $request) $key = $this->getCacheKey($request); if (isset($this->locks[$key])) { - flock($this->locks[$key], LOCK_UN); + flock($this->locks[$key], \LOCK_UN); fclose($this->locks[$key]); unset($this->locks[$key]); @@ -110,13 +122,13 @@ public function isLocked(Request $request) return true; // shortcut if lock held by this process } - if (!file_exists($path = $this->getPath($key))) { + if (!is_file($path = $this->getPath($key))) { return false; } - $h = fopen($path, 'rb'); - flock($h, LOCK_EX | LOCK_NB, $wouldBlock); - flock($h, LOCK_UN); // release the lock we just acquired + $h = fopen($path, 'r'); + flock($h, \LOCK_EX | \LOCK_NB, $wouldBlock); + flock($h, \LOCK_UN); // release the lock we just acquired fclose($h); return (bool) $wouldBlock; @@ -125,7 +137,7 @@ public function isLocked(Request $request) /** * Locates a cached Response for the Request provided. * - * @return Response|null A Response instance, or null if no cache entry was found + * @return Response|null */ public function lookup(Request $request) { @@ -166,7 +178,7 @@ public function lookup(Request $request) * Existing entries are read and any that match the response are removed. This * method calls write with the new list of cache entries. * - * @return string The key under which the response is stored + * @return string * * @throws \RuntimeException */ @@ -215,6 +227,10 @@ public function write(Request $request, Response $response) $headers = $this->persistResponse($response); unset($headers['age']); + foreach ($this->options['private_headers'] as $h) { + unset($headers[strtolower($h)]); + } + array_unshift($entries, [$storedEnv, $headers]); if (!$this->save($key, serialize($entries))) { @@ -277,8 +293,8 @@ private function requestsMatch(?string $vary, array $env1, array $env2): bool foreach (preg_split('/[\s,]+/', $vary) as $header) { $key = str_replace('_', '-', strtolower($header)); - $v1 = isset($env1[$key]) ? $env1[$key] : null; - $v2 = isset($env2[$key]) ? $env2[$key] : null; + $v1 = $env1[$key] ?? null; + $v2 = $env2[$key] ?? null; if ($v1 !== $v2) { return false; } @@ -298,7 +314,7 @@ private function getMetadata(string $key): array return []; } - return unserialize($entries); + return unserialize($entries) ?: []; } /** @@ -326,12 +342,12 @@ private function doPurge(string $url): bool { $key = $this->getCacheKey(Request::create($url)); if (isset($this->locks[$key])) { - flock($this->locks[$key], LOCK_UN); + flock($this->locks[$key], \LOCK_UN); fclose($this->locks[$key]); unset($this->locks[$key]); } - if (file_exists($path = $this->getPath($key))) { + if (is_file($path = $this->getPath($key))) { unlink($path); return true; @@ -347,7 +363,7 @@ private function load(string $key): ?string { $path = $this->getPath($key); - return file_exists($path) && false !== ($contents = file_get_contents($path)) ? $contents : null; + return is_file($path) && false !== ($contents = @file_get_contents($path)) ? $contents : null; } /** @@ -372,12 +388,12 @@ private function save(string $key, string $data, bool $overwrite = true): bool return false; } } else { - if (!file_exists(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) { + if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) { return false; } $tmpFile = tempnam(\dirname($path), basename($path)); - if (false === $fp = @fopen($tmpFile, 'wb')) { + if (false === $fp = @fopen($tmpFile, 'w')) { @unlink($tmpFile); return false; @@ -418,7 +434,7 @@ public function getPath(string $key) * headers, use a Vary header to indicate them, and each representation will * be stored independently under the same cache key. * - * @return string A key for the given Request + * @return string */ protected function generateCacheKey(Request $request) { diff --git a/HttpCache/StoreInterface.php b/HttpCache/StoreInterface.php index 49d88c27da..3d07ef3fc3 100644 --- a/HttpCache/StoreInterface.php +++ b/HttpCache/StoreInterface.php @@ -27,7 +27,7 @@ interface StoreInterface /** * Locates a cached Response for the Request provided. * - * @return Response|null A Response instance, or null if no cache entry was found + * @return Response|null */ public function lookup(Request $request); diff --git a/HttpCache/SubRequestHandler.php b/HttpCache/SubRequestHandler.php index 294b964acc..253071f07d 100644 --- a/HttpCache/SubRequestHandler.php +++ b/HttpCache/SubRequestHandler.php @@ -31,13 +31,14 @@ public static function handle(HttpKernelInterface $kernel, Request $request, int // remove untrusted values $remoteAddr = $request->server->get('REMOTE_ADDR'); - if (!IpUtils::checkIp($remoteAddr, $trustedProxies)) { + if (!$remoteAddr || !IpUtils::checkIp($remoteAddr, $trustedProxies)) { $trustedHeaders = [ 'FORWARDED' => $trustedHeaderSet & Request::HEADER_FORWARDED, 'X_FORWARDED_FOR' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_FOR, 'X_FORWARDED_HOST' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_HOST, 'X_FORWARDED_PROTO' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PROTO, 'X_FORWARDED_PORT' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PORT, + 'X_FORWARDED_PREFIX' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PREFIX, ]; foreach (array_filter($trustedHeaders) as $name => $key) { $request->headers->remove($name); diff --git a/HttpCache/SurrogateInterface.php b/HttpCache/SurrogateInterface.php index 53a7e2a6b4..3f3c74a97a 100644 --- a/HttpCache/SurrogateInterface.php +++ b/HttpCache/SurrogateInterface.php @@ -26,14 +26,14 @@ public function getName(); /** * Returns a new cache strategy instance. * - * @return ResponseCacheStrategyInterface A ResponseCacheStrategyInterface instance + * @return ResponseCacheStrategyInterface */ public function createCacheStrategy(); /** * Checks that at least one surrogate has Surrogate capability. * - * @return bool true if one surrogate has Surrogate capability, false otherwise + * @return bool */ public function hasSurrogateCapability(Request $request); @@ -52,7 +52,7 @@ public function addSurrogateControl(Response $response); /** * Checks that the Response needs to be parsed for Surrogate tags. * - * @return bool true if the Response needs to be parsed, false otherwise + * @return bool */ public function needsParsing(Response $response); diff --git a/HttpClientKernel.php b/HttpClientKernel.php index a5e207d546..58ca82e5a8 100644 --- a/HttpClientKernel.php +++ b/HttpClientKernel.php @@ -35,14 +35,14 @@ final class HttpClientKernel implements HttpKernelInterface public function __construct(HttpClientInterface $client = null) { - if (!class_exists(HttpClient::class)) { + if (null === $client && !class_exists(HttpClient::class)) { throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); } $this->client = $client ?? HttpClient::create(); } - public function handle(Request $request, int $type = HttpKernelInterface::MASTER_REQUEST, bool $catch = true): Response + public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $headers = $this->getHeaders($request); $body = ''; @@ -53,7 +53,6 @@ public function handle(Request $request, int $type = HttpKernelInterface::MASTER $response = $this->client->request($request->getMethod(), $request->getUri(), [ 'headers' => $headers, 'body' => $body, - 'max_redirects' => 0, ] + $request->attributes->get('http_client_options', [])); $response = new Response($response->getContent(!$catch), $response->getStatusCode(), $response->getHeaders(!$catch)); diff --git a/HttpKernel.php b/HttpKernel.php index 850331349b..38102e2525 100644 --- a/HttpKernel.php +++ b/HttpKernel.php @@ -33,7 +33,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; // Help opcache.preload discover always-needed symbols -class_exists(LegacyEventDispatcherProxy::class); class_exists(ControllerArgumentsEvent::class); class_exists(ControllerEvent::class); class_exists(ExceptionEvent::class); @@ -60,21 +59,18 @@ public function __construct(EventDispatcherInterface $dispatcher, ControllerReso { $this->dispatcher = $dispatcher; $this->resolver = $resolver; - $this->requestStack = $requestStack ?: new RequestStack(); - $this->argumentResolver = $argumentResolver; - - if (null === $this->argumentResolver) { - $this->argumentResolver = new ArgumentResolver(); - } + $this->requestStack = $requestStack ?? new RequestStack(); + $this->argumentResolver = $argumentResolver ?? new ArgumentResolver(); } /** * {@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) { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); + $this->requestStack->push($request); try { return $this->handleRaw($request, $type); } catch (\Exception $e) { @@ -88,6 +84,8 @@ public function handle(Request $request, int $type = HttpKernelInterface::MASTER } return $this->handleThrowable($e, $request, $type); + } finally { + $this->requestStack->pop(); } } @@ -104,11 +102,21 @@ public function terminate(Request $request, Response $response) */ public function terminateWithException(\Throwable $exception, Request $request = null) { - if (!$request = $request ?: $this->requestStack->getMasterRequest()) { + if (!$request = $request ?: $this->requestStack->getMainRequest()) { throw $exception; } - $response = $this->handleThrowable($exception, $request, self::MASTER_REQUEST); + if ($pop = $request !== $this->requestStack->getMainRequest()) { + $this->requestStack->push($request); + } + + try { + $response = $this->handleThrowable($exception, $request, self::MAIN_REQUEST); + } finally { + if ($pop) { + $this->requestStack->pop(); + } + } $response->sendHeaders(); $response->sendContent(); @@ -124,10 +132,8 @@ public function terminateWithException(\Throwable $exception, Request $request = * @throws \LogicException If one of the listener does not behave as expected * @throws NotFoundHttpException When controller cannot be found */ - private function handleRaw(Request $request, int $type = self::MASTER_REQUEST): Response + private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response { - $this->requestStack->push($request); - // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); @@ -204,7 +210,6 @@ private function filterResponse(Response $response, Request $request, int $type) private function finishRequest(Request $request, int $type) { $this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type), KernelEvents::FINISH_REQUEST); - $this->requestStack->pop(); } /** diff --git a/HttpKernelBrowser.php b/HttpKernelBrowser.php index d28be99741..643134f1bb 100644 --- a/HttpKernelBrowser.php +++ b/HttpKernelBrowser.php @@ -25,8 +25,8 @@ * * @author Fabien Potencier * - * @method Request getRequest() A Request instance - * @method Response getResponse() A Response instance + * @method Request getRequest() + * @method Response getResponse() */ class HttpKernelBrowser extends AbstractBrowser { @@ -54,13 +54,15 @@ public function catchExceptions(bool $catchExceptions) } /** - * Makes a request. + * {@inheritdoc} * - * @return Response A Response instance + * @param Request $request + * + * @return Response */ - protected function doRequest($request) + protected function doRequest(object $request) { - $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $this->catchExceptions); + $response = $this->kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, $this->catchExceptions); if ($this->kernel instanceof TerminableInterface) { $this->kernel->terminate($request, $response); @@ -70,11 +72,13 @@ protected function doRequest($request) } /** - * Returns the script to execute when the request must be insulated. + * {@inheritdoc} + * + * @param Request $request * * @return string */ - protected function getScript($request) + protected function getScript(object $request) { $kernel = var_export(serialize($this->kernel), true); $request = var_export(serialize($request), true); @@ -124,13 +128,16 @@ protected function getHandleScript() } /** - * Converts the BrowserKit request to a HttpKernel request. + * {@inheritdoc} * - * @return Request A Request instance + * @return Request */ protected function filterRequest(DomRequest $request) { - $httpRequest = Request::create($request->getUri(), $request->getMethod(), $request->getParameters(), $request->getCookies(), $request->getFiles(), $request->getServer(), $request->getContent()); + $httpRequest = Request::create($request->getUri(), $request->getMethod(), $request->getParameters(), $request->getCookies(), $request->getFiles(), $server = $request->getServer(), $request->getContent()); + if (!isset($server['HTTP_ACCEPT'])) { + $httpRequest->headers->remove('Accept'); + } foreach ($this->filterFiles($httpRequest->files->all()) as $key => $value) { $httpRequest->files->set($key, $value); @@ -150,7 +157,7 @@ protected function filterRequest(DomRequest $request) * * @see UploadedFile * - * @return array An array with all uploaded files marked as already moved + * @return array */ protected function filterFiles(array $files) { @@ -164,7 +171,7 @@ protected function filterFiles(array $files) '', $value->getClientOriginalName(), $value->getClientMimeType(), - UPLOAD_ERR_INI_SIZE, + \UPLOAD_ERR_INI_SIZE, true ); } else { @@ -183,11 +190,13 @@ protected function filterFiles(array $files) } /** - * Converts the HttpKernel response to a BrowserKit response. + * {@inheritdoc} + * + * @param Response $response * - * @return DomResponse A DomResponse instance + * @return DomResponse */ - protected function filterResponse($response) + protected function filterResponse(object $response) { // this is needed to support StreamedResponse ob_start(); diff --git a/HttpKernelInterface.php b/HttpKernelInterface.php index a75d1a8724..0449240e7e 100644 --- a/HttpKernelInterface.php +++ b/HttpKernelInterface.php @@ -21,8 +21,14 @@ */ interface HttpKernelInterface { - const MASTER_REQUEST = 1; - const SUB_REQUEST = 2; + public const MAIN_REQUEST = 1; + public const SUB_REQUEST = 2; + + /** + * @deprecated since symfony/http-kernel 5.3, use MAIN_REQUEST instead. + * To ease the migration, this constant won't be removed until Symfony 7.0. + */ + public const MASTER_REQUEST = self::MAIN_REQUEST; /** * Handles a Request to convert it to a Response. @@ -31,12 +37,12 @@ interface HttpKernelInterface * and do its best to convert them to a Response instance. * * @param int $type The type of the request - * (one of HttpKernelInterface::MASTER_REQUEST or HttpKernelInterface::SUB_REQUEST) + * (one of HttpKernelInterface::MAIN_REQUEST or HttpKernelInterface::SUB_REQUEST) * @param bool $catch Whether to catch exceptions or not * - * @return Response A Response instance + * @return Response * * @throws \Exception When an Exception occurs during processing */ - public function handle(Request $request, int $type = self::MASTER_REQUEST, bool $catch = true); + public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true); } diff --git a/Kernel.php b/Kernel.php index c90c77ae77..b2ccc7d95a 100644 --- a/Kernel.php +++ b/Kernel.php @@ -13,6 +13,7 @@ use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; +use Symfony\Component\Config\Builder\ConfigBuilderGenerator; use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\Loader\DelegatingLoader; use Symfony\Component\Config\Loader\LoaderResolver; @@ -23,6 +24,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\Dumper\Preloader; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; @@ -56,7 +58,7 @@ class_exists(ConfigCache::class); abstract class Kernel implements KernelInterface, RebootableInterface, TerminableInterface { /** - * @var BundleInterface[] + * @var array */ protected $bundles = []; @@ -71,21 +73,27 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; + /** + * @var array + */ private static $freshCache = []; - const VERSION = '5.1.5'; - const VERSION_ID = 50105; - const MAJOR_VERSION = 5; - const MINOR_VERSION = 1; - const RELEASE_VERSION = 5; - const EXTRA_VERSION = ''; + public const VERSION = '5.4.20'; + public const VERSION_ID = 50420; + public const MAJOR_VERSION = 5; + public const MINOR_VERSION = 4; + public const RELEASE_VERSION = 20; + public const EXTRA_VERSION = ''; - const END_OF_MAINTENANCE = '01/2021'; - const END_OF_LIFE = '01/2021'; + public const END_OF_MAINTENANCE = '11/2024'; + public const END_OF_LIFE = '11/2025'; public function __construct(string $environment, bool $debug) { - $this->environment = $environment; + if (!$this->environment = $environment) { + throw new \InvalidArgumentException(sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this))); + } + $this->debug = $debug; } @@ -115,20 +123,10 @@ public function boot() return; } - if ($this->debug) { - $this->startTime = microtime(true); - } - if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) { - putenv('SHELL_VERBOSITY=3'); - $_ENV['SHELL_VERBOSITY'] = 3; - $_SERVER['SHELL_VERBOSITY'] = 3; - } - - // init bundles - $this->initializeBundles(); - // init container - $this->initializeContainer(); + if (null === $this->container) { + $this->preBoot(); + } foreach ($this->getBundles() as $bundle) { $bundle->setContainer($this->container); @@ -186,8 +184,16 @@ public function shutdown() /** * {@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) { + if (!$this->booted) { + $container = $this->container ?? $this->preBoot(); + + if ($container->has('http_cache')) { + return $container->get('http_cache')->handle($request, $type, $catch); + } + } + $this->boot(); ++$this->requestStackSize; $this->resetServices = true; @@ -200,7 +206,7 @@ public function handle(Request $request, int $type = HttpKernelInterface::MASTER } /** - * Gets a HTTP kernel from the container. + * Gets an HTTP kernel from the container. * * @return HttpKernelInterface */ @@ -238,14 +244,14 @@ public function locateResource(string $name) throw new \InvalidArgumentException(sprintf('A resource name must start with @ ("%s" given).', $name)); } - if (false !== strpos($name, '..')) { + if (str_contains($name, '..')) { throw new \RuntimeException(sprintf('File name "%s" contains invalid characters (..).', $name)); } $bundleName = substr($name, 1); $path = ''; - if (false !== strpos($bundleName, '/')) { - list($bundleName, $path) = explode('/', $bundleName, 2); + if (str_contains($bundleName, '/')) { + [$bundleName, $path] = explode('/', $bundleName, 2); } $bundle = $this->getBundle($bundleName); @@ -275,7 +281,7 @@ public function isDebug() /** * Gets the application root dir (path of the project's composer file). * - * @return string The project root dir + * @return string */ public function getProjectDir() { @@ -316,7 +322,7 @@ public function getContainer() */ public function setAnnotatedClassCache(array $annotatedClasses) { - file_put_contents(($this->warmupDir ?: $this->getCacheDir()).'/annotations.map', sprintf('warmupDir ?: $this->getBuildDir()).'/annotations.map', sprintf('debug && null !== $this->startTime ? $this->startTime : -INF; + return $this->debug && null !== $this->startTime ? $this->startTime : -\INF; } /** @@ -335,6 +341,15 @@ public function getCacheDir() return $this->getProjectDir().'/var/cache/'.$this->environment; } + /** + * {@inheritdoc} + */ + public function getBuildDir(): string + { + // Returns $this->getCacheDir() for backward compatibility + return $this->getCacheDir(); + } + /** * {@inheritdoc} */ @@ -391,12 +406,12 @@ protected function build(ContainerBuilder $container) * * @throws \InvalidArgumentException If the generated classname is invalid * - * @return string The container class + * @return string */ protected function getContainerClass() { $class = static::class; - $class = false !== strpos($class, "@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class; + $class = str_contains($class, "@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class; $class = str_replace('\\', '_', $class).ucfirst($this->environment).($this->debug ? 'Debug' : '').'Container'; if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) { @@ -421,18 +436,18 @@ protected function getContainerBaseClass() /** * Initializes the service container. * - * The cached version of the service container is used when fresh, otherwise the + * The built version of the service container is used when fresh, otherwise the * container is built. */ protected function initializeContainer() { $class = $this->getContainerClass(); - $cacheDir = $this->warmupDir ?: $this->getCacheDir(); - $cache = new ConfigCache($cacheDir.'/'.$class.'.php', $this->debug); + $buildDir = $this->warmupDir ?: $this->getBuildDir(); + $cache = new ConfigCache($buildDir.'/'.$class.'.php', $this->debug); $cachePath = $cache->getPath(); // Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors - $errorLevel = error_reporting(E_ALL ^ E_WARNING); + $errorLevel = error_reporting(\E_ALL ^ \E_WARNING); try { if (is_file($cachePath) && \is_object($this->container = include $cachePath) @@ -450,18 +465,16 @@ protected function initializeContainer() $oldContainer = \is_object($this->container) ? new \ReflectionClass($this->container) : $this->container = null; try { - is_dir($cacheDir) ?: mkdir($cacheDir, 0777, true); + is_dir($buildDir) ?: mkdir($buildDir, 0777, true); if ($lock = fopen($cachePath.'.lock', 'w')) { - flock($lock, LOCK_EX | LOCK_NB, $wouldBlock); - - if (!flock($lock, $wouldBlock ? LOCK_SH : LOCK_EX)) { + if (!flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock) && !flock($lock, $wouldBlock ? \LOCK_SH : \LOCK_EX)) { fclose($lock); $lock = null; - } elseif (!\is_object($this->container = include $cachePath)) { + } elseif (!is_file($cachePath) || !\is_object($this->container = include $cachePath)) { $this->container = null; } elseif (!$oldContainer || \get_class($this->container) !== $oldContainer->name) { - flock($lock, LOCK_UN); + flock($lock, \LOCK_UN); fclose($lock); $this->container->set('kernel', $this); @@ -476,7 +489,7 @@ protected function initializeContainer() if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) { $collectedLogs = []; $previousHandler = set_error_handler(function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { - if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { + if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; } @@ -486,7 +499,7 @@ protected function initializeContainer() return null; } - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5); + $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5); // Clean the trace by removing first frames added by the error handler itself. for ($i = 0; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { @@ -535,15 +548,15 @@ protected function initializeContainer() if ($collectDeprecations) { restore_error_handler(); - file_put_contents($cacheDir.'/'.$class.'Deprecations.log', serialize(array_values($collectedLogs))); - file_put_contents($cacheDir.'/'.$class.'Compiler.log', null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''); + @file_put_contents($buildDir.'/'.$class.'Deprecations.log', serialize(array_values($collectedLogs))); + @file_put_contents($buildDir.'/'.$class.'Compiler.log', null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''); } } $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); if ($lock) { - flock($lock, LOCK_UN); + flock($lock, \LOCK_UN); fclose($lock); } @@ -557,7 +570,7 @@ protected function initializeContainer() static $legacyContainers = []; $oldContainerDir = \dirname($oldContainer->getFileName()); $legacyContainers[$oldContainerDir.'.legacy'] = true; - foreach (glob(\dirname($oldContainerDir).\DIRECTORY_SEPARATOR.'*.legacy', GLOB_NOSORT) as $legacyContainer) { + foreach (glob(\dirname($oldContainerDir).\DIRECTORY_SEPARATOR.'*.legacy', \GLOB_NOSORT) as $legacyContainer) { if (!isset($legacyContainers[$legacyContainer]) && @unlink($legacyContainer)) { (new Filesystem())->remove(substr($legacyContainer, 0, -7)); } @@ -572,7 +585,7 @@ protected function initializeContainer() $preload = array_merge($preload, (array) $this->container->get('cache_warmer')->warmUp($this->container->getParameter('kernel.cache_dir'))); } - if ($preload && method_exists(Preloader::class, 'append') && file_exists($preloadFile = $cacheDir.'/'.$class.'.preload.php')) { + if ($preload && method_exists(Preloader::class, 'append') && file_exists($preloadFile = $buildDir.'/'.$class.'.preload.php')) { Preloader::append($preloadFile, $preload); } } @@ -580,7 +593,7 @@ protected function initializeContainer() /** * Returns the kernel parameters. * - * @return array An array of kernel parameters + * @return array */ protected function getKernelParameters() { @@ -598,8 +611,10 @@ protected function getKernelParameters() return [ 'kernel.project_dir' => realpath($this->getProjectDir()) ?: $this->getProjectDir(), 'kernel.environment' => $this->environment, + 'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%', 'kernel.debug' => $this->debug, - 'kernel.cache_dir' => realpath($cacheDir = $this->warmupDir ?: $this->getCacheDir()) ?: $cacheDir, + 'kernel.build_dir' => realpath($buildDir = $this->warmupDir ?: $this->getBuildDir()) ?: $buildDir, + 'kernel.cache_dir' => realpath($cacheDir = ($this->getCacheDir() === $this->getBuildDir() ? ($this->warmupDir ?: $this->getCacheDir()) : $this->getCacheDir())) ?: $cacheDir, 'kernel.logs_dir' => realpath($this->getLogDir()) ?: $this->getLogDir(), 'kernel.bundles' => $bundles, 'kernel.bundles_metadata' => $bundlesMetadata, @@ -611,13 +626,13 @@ protected function getKernelParameters() /** * Builds the service container. * - * @return ContainerBuilder The compiled service container + * @return ContainerBuilder * * @throws \RuntimeException */ protected function buildContainer() { - foreach (['cache' => $this->warmupDir ?: $this->getCacheDir(), 'logs' => $this->getLogDir()] as $name => $dir) { + foreach (['cache' => $this->getCacheDir(), 'build' => $this->warmupDir ?: $this->getBuildDir(), 'logs' => $this->getLogDir()] as $name => $dir) { if (!is_dir($dir)) { if (false === @mkdir($dir, 0777, true) && !is_dir($dir)) { throw new \RuntimeException(sprintf('Unable to create the "%s" directory (%s).', $name, $dir)); @@ -632,6 +647,7 @@ protected function buildContainer() $this->prepareContainer($container); if (null !== $cont = $this->registerContainerConfiguration($this->getContainerLoader($container))) { + trigger_deprecation('symfony/http-kernel', '5.3', 'Returning a ContainerBuilder from "%s::registerContainerConfiguration()" is deprecated.', get_debug_type($this)); $container->merge($cont); } @@ -680,10 +696,13 @@ protected function getContainerBuilder() $container = new ContainerBuilder(); $container->getParameterBag()->add($this->getKernelParameters()); + if ($this instanceof ExtensionInterface) { + $container->registerExtension($this); + } if ($this instanceof CompilerPassInterface) { $container->addCompilerPass($this, PassConfig::TYPE_BEFORE_OPTIMIZATION, -10000); } - if (class_exists('ProxyManager\Configuration') && class_exists('Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator')) { + if (class_exists(\ProxyManager\Configuration::class) && class_exists(RuntimeInstantiator::class)) { $container->setProxyInstantiator(new RuntimeInstantiator()); } @@ -701,7 +720,7 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container // cache the container $dumper = new PhpDumper($container); - if (class_exists('ProxyManager\Configuration') && class_exists('Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper')) { + if (class_exists(\ProxyManager\Configuration::class) && class_exists(ProxyDumper::class)) { $dumper->setProxyDumper(new ProxyDumper()); } @@ -724,7 +743,7 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container @chmod($dir.$file, 0666 & ~umask()); } $legacyFile = \dirname($dir.key($content)).'.legacy'; - if (file_exists($legacyFile)) { + if (is_file($legacyFile)) { @unlink($legacyFile); } @@ -734,31 +753,59 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container /** * Returns a loader for the container. * - * @return DelegatingLoader The loader + * @return DelegatingLoader */ protected function getContainerLoader(ContainerInterface $container) { + $env = $this->getEnvironment(); $locator = new FileLocator($this); $resolver = new LoaderResolver([ - new XmlFileLoader($container, $locator), - new YamlFileLoader($container, $locator), - new IniFileLoader($container, $locator), - new PhpFileLoader($container, $locator), - new GlobFileLoader($container, $locator), - new DirectoryLoader($container, $locator), - new ClosureLoader($container), + new XmlFileLoader($container, $locator, $env), + new YamlFileLoader($container, $locator, $env), + new IniFileLoader($container, $locator, $env), + new PhpFileLoader($container, $locator, $env, class_exists(ConfigBuilderGenerator::class) ? new ConfigBuilderGenerator($this->getBuildDir()) : null), + new GlobFileLoader($container, $locator, $env), + new DirectoryLoader($container, $locator, $env), + new ClosureLoader($container, $env), ]); return new DelegatingLoader($resolver); } + private function preBoot(): ContainerInterface + { + if ($this->debug) { + $this->startTime = microtime(true); + } + if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) { + putenv('SHELL_VERBOSITY=3'); + $_ENV['SHELL_VERBOSITY'] = 3; + $_SERVER['SHELL_VERBOSITY'] = 3; + } + + $this->initializeBundles(); + $this->initializeContainer(); + + $container = $this->container; + + if ($container->hasParameter('kernel.trusted_hosts') && $trustedHosts = $container->getParameter('kernel.trusted_hosts')) { + Request::setTrustedHosts($trustedHosts); + } + + if ($container->hasParameter('kernel.trusted_proxies') && $container->hasParameter('kernel.trusted_headers') && $trustedProxies = $container->getParameter('kernel.trusted_proxies')) { + Request::setTrustedProxies(\is_array($trustedProxies) ? $trustedProxies : array_map('trim', explode(',', $trustedProxies)), $container->getParameter('kernel.trusted_headers')); + } + + return $container; + } + /** * Removes comments from a PHP source string. * * We don't use the PHP php_strip_whitespace() function * as we want the content to be readable and well-formatted. * - * @return string The PHP string with the comments removed + * @return string */ public static function stripComments(string $source) { @@ -774,14 +821,14 @@ public static function stripComments(string $source) $token = $tokens[$i]; if (!isset($token[1]) || 'b"' === $token) { $rawChunk .= $token; - } elseif (T_START_HEREDOC === $token[0]) { + } elseif (\T_START_HEREDOC === $token[0]) { $output .= $rawChunk.$token[1]; do { $token = $tokens[++$i]; $output .= isset($token[1]) && 'b"' !== $token ? $token[1] : $token; - } while (T_END_HEREDOC !== $token[0]); + } while (\T_END_HEREDOC !== $token[0]); $rawChunk = ''; - } elseif (T_WHITESPACE === $token[0]) { + } elseif (\T_WHITESPACE === $token[0]) { if ($ignoreSpace) { $ignoreSpace = false; @@ -790,14 +837,19 @@ public static function stripComments(string $source) // replace multiple new lines with a single newline $rawChunk .= preg_replace(['/\n{2,}/S'], "\n", $token[1]); - } elseif (\in_array($token[0], [T_COMMENT, T_DOC_COMMENT])) { + } elseif (\in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT])) { + if (!\in_array($rawChunk[\strlen($rawChunk) - 1], [' ', "\n", "\r", "\t"], true)) { + $rawChunk .= ' '; + } $ignoreSpace = true; } else { $rawChunk .= $token[1]; // The PHP-open tag already has a new-line - if (T_OPEN_TAG === $token[0]) { + if (\T_OPEN_TAG === $token[0]) { $ignoreSpace = true; + } else { + $ignoreSpace = false; } } } @@ -820,6 +872,10 @@ public function __sleep() public function __wakeup() { + if (\is_object($this->environment) || \is_object($this->debug)) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + $this->__construct($this->environment, $this->debug); } } diff --git a/KernelEvents.php b/KernelEvents.php index 0e1c9083e5..3d47f60801 100644 --- a/KernelEvents.php +++ b/KernelEvents.php @@ -11,6 +11,15 @@ namespace Symfony\Component\HttpKernel; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\Event\TerminateEvent; +use Symfony\Component\HttpKernel\Event\ViewEvent; + /** * Contains all events thrown in the HttpKernel component. * @@ -27,7 +36,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\RequestEvent") */ - const REQUEST = 'kernel.request'; + public const REQUEST = 'kernel.request'; /** * The EXCEPTION event occurs when an uncaught exception appears. @@ -37,7 +46,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\ExceptionEvent") */ - const EXCEPTION = 'kernel.exception'; + public const EXCEPTION = 'kernel.exception'; /** * The CONTROLLER event occurs once a controller was found for @@ -48,7 +57,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\ControllerEvent") */ - const CONTROLLER = 'kernel.controller'; + public const CONTROLLER = 'kernel.controller'; /** * The CONTROLLER_ARGUMENTS event occurs once controller arguments have been resolved. @@ -58,7 +67,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent") */ - const CONTROLLER_ARGUMENTS = 'kernel.controller_arguments'; + public const CONTROLLER_ARGUMENTS = 'kernel.controller_arguments'; /** * The VIEW event occurs when the return value of a controller @@ -69,7 +78,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\ViewEvent") */ - const VIEW = 'kernel.view'; + public const VIEW = 'kernel.view'; /** * The RESPONSE event occurs once a response was created for @@ -80,7 +89,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\ResponseEvent") */ - const RESPONSE = 'kernel.response'; + public const RESPONSE = 'kernel.response'; /** * The FINISH_REQUEST event occurs when a response was generated for a request. @@ -90,7 +99,7 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\FinishRequestEvent") */ - const FINISH_REQUEST = 'kernel.finish_request'; + public const FINISH_REQUEST = 'kernel.finish_request'; /** * The TERMINATE event occurs once a response was sent. @@ -99,5 +108,21 @@ final class KernelEvents * * @Event("Symfony\Component\HttpKernel\Event\TerminateEvent") */ - const TERMINATE = 'kernel.terminate'; + public const TERMINATE = 'kernel.terminate'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + public const ALIASES = [ + ControllerArgumentsEvent::class => self::CONTROLLER_ARGUMENTS, + ControllerEvent::class => self::CONTROLLER, + ResponseEvent::class => self::RESPONSE, + FinishRequestEvent::class => self::FINISH_REQUEST, + RequestEvent::class => self::REQUEST, + ViewEvent::class => self::VIEW, + ExceptionEvent::class => self::EXCEPTION, + TerminateEvent::class => self::TERMINATE, + ]; } diff --git a/KernelInterface.php b/KernelInterface.php index cea86f687a..41135a6d80 100644 --- a/KernelInterface.php +++ b/KernelInterface.php @@ -20,6 +20,10 @@ * * It manages an environment made of application kernel and bundles. * + * @method string getBuildDir() Returns the build directory - not implementing it is deprecated since Symfony 5.2. + * This directory should be used to store build artifacts, and can be read-only at runtime. + * Caches written at runtime should be stored in the "cache directory" ({@see KernelInterface::getCacheDir()}). + * * @author Fabien Potencier */ interface KernelInterface extends HttpKernelInterface @@ -27,7 +31,7 @@ interface KernelInterface extends HttpKernelInterface /** * Returns an array of bundles to register. * - * @return iterable|BundleInterface[] An iterable of bundle instances + * @return iterable */ public function registerBundles(); @@ -51,14 +55,14 @@ public function shutdown(); /** * Gets the registered bundle instances. * - * @return BundleInterface[] An array of registered bundle instances + * @return array */ public function getBundles(); /** * Returns a bundle. * - * @return BundleInterface A BundleInterface instance + * @return BundleInterface * * @throws \InvalidArgumentException when the bundle is not enabled */ @@ -76,7 +80,7 @@ public function getBundle(string $name); * where BundleName is the name of the bundle * and the remaining part is the relative path in the bundle. * - * @return string The absolute path of the resource + * @return string * * @throws \InvalidArgumentException if the file cannot be found or the name is not valid * @throws \RuntimeException if the name contains invalid/unsafe characters @@ -86,14 +90,14 @@ public function locateResource(string $name); /** * Gets the environment. * - * @return string The current environment + * @return string */ public function getEnvironment(); /** * Checks if debug mode is enabled. * - * @return bool true if debug mode is enabled, false otherwise + * @return bool */ public function isDebug(); @@ -114,28 +118,32 @@ public function getContainer(); /** * Gets the request start time (not available if debug is disabled). * - * @return float The request start timestamp + * @return float */ public function getStartTime(); /** * Gets the cache directory. * - * @return string The cache directory + * Since Symfony 5.2, the cache directory should be used for caches that are written at runtime. + * For caches and artifacts that can be warmed at compile-time and deployed as read-only, + * use the new "build directory" returned by the {@see getBuildDir()} method. + * + * @return string */ public function getCacheDir(); /** * Gets the log directory. * - * @return string The log directory + * @return string */ public function getLogDir(); /** * Gets the charset of the application. * - * @return string The charset + * @return string */ public function getCharset(); } diff --git a/LICENSE b/LICENSE index 9e936ec044..0083704572 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2020 Fabien Potencier +Copyright (c) 2004-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Log/DebugLoggerInterface.php b/Log/DebugLoggerInterface.php index 2c2eae63ee..19ff0db181 100644 --- a/Log/DebugLoggerInterface.php +++ b/Log/DebugLoggerInterface.php @@ -27,14 +27,14 @@ interface DebugLoggerInterface * timestamp, message, priority, and priorityName. * It can also have an optional context key containing an array. * - * @return array An array of logs + * @return array */ public function getLogs(Request $request = null); /** * Returns the number of errors. * - * @return int The number of errors + * @return int */ public function countErrors(Request $request = null); diff --git a/Log/Logger.php b/Log/Logger.php index 1e6308d6e0..c2a45bb951 100644 --- a/Log/Logger.php +++ b/Log/Logger.php @@ -22,7 +22,7 @@ */ class Logger extends AbstractLogger { - private static $levels = [ + private const LEVELS = [ LogLevel::DEBUG => 0, LogLevel::INFO => 1, LogLevel::NOTICE => 2, @@ -35,28 +35,37 @@ class Logger extends AbstractLogger private $minLevelIndex; private $formatter; + + /** @var resource|null */ private $handle; + /** + * @param string|resource|null $output + */ public function __construct(string $minLevel = null, $output = null, callable $formatter = null) { if (null === $minLevel) { $minLevel = null === $output || 'php://stdout' === $output || 'php://stderr' === $output ? LogLevel::ERROR : LogLevel::WARNING; if (isset($_ENV['SHELL_VERBOSITY']) || isset($_SERVER['SHELL_VERBOSITY'])) { - switch ((int) (isset($_ENV['SHELL_VERBOSITY']) ? $_ENV['SHELL_VERBOSITY'] : $_SERVER['SHELL_VERBOSITY'])) { - case -1: $minLevel = LogLevel::ERROR; break; - case 1: $minLevel = LogLevel::NOTICE; break; - case 2: $minLevel = LogLevel::INFO; break; - case 3: $minLevel = LogLevel::DEBUG; break; + switch ((int) ($_ENV['SHELL_VERBOSITY'] ?? $_SERVER['SHELL_VERBOSITY'])) { + case -1: $minLevel = LogLevel::ERROR; + break; + case 1: $minLevel = LogLevel::NOTICE; + break; + case 2: $minLevel = LogLevel::INFO; + break; + case 3: $minLevel = LogLevel::DEBUG; + break; } } } - if (!isset(self::$levels[$minLevel])) { + if (!isset(self::LEVELS[$minLevel])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel)); } - $this->minLevelIndex = self::$levels[$minLevel]; + $this->minLevelIndex = self::LEVELS[$minLevel]; $this->formatter = $formatter ?: [$this, 'format']; if ($output && false === $this->handle = \is_resource($output) ? $output : @fopen($output, 'a')) { throw new InvalidArgumentException(sprintf('Unable to open "%s".', $output)); @@ -70,17 +79,17 @@ public function __construct(string $minLevel = null, $output = null, callable $f */ public function log($level, $message, array $context = []) { - if (!isset(self::$levels[$level])) { + if (!isset(self::LEVELS[$level])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); } - if (self::$levels[$level] < $this->minLevelIndex) { + if (self::LEVELS[$level] < $this->minLevelIndex) { return; } $formatter = $this->formatter; if ($this->handle) { - @fwrite($this->handle, $formatter($level, $message, $context)); + @fwrite($this->handle, $formatter($level, $message, $context).\PHP_EOL); } else { error_log($formatter($level, $message, $context, false)); } @@ -88,10 +97,10 @@ public function log($level, $message, array $context = []) private function format(string $level, string $message, array $context, bool $prefixDate = true): string { - if (false !== strpos($message, '{')) { + if (str_contains($message, '{')) { $replacements = []; foreach ($context as $key => $val) { - if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { + if (null === $val || \is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof \DateTimeInterface) { $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); @@ -105,7 +114,7 @@ private function format(string $level, string $message, array $context, bool $pr $message = strtr($message, $replacements); } - $log = sprintf('[%s] %s', $level, $message).PHP_EOL; + $log = sprintf('[%s] %s', $level, $message); if ($prefixDate) { $log = date(\DateTime::RFC3339).' '.$log; } diff --git a/Profiler/FileProfilerStorage.php b/Profiler/FileProfilerStorage.php index 3b5dfbf26f..0b5a780abd 100644 --- a/Profiler/FileProfilerStorage.php +++ b/Profiler/FileProfilerStorage.php @@ -34,7 +34,7 @@ class FileProfilerStorage implements ProfilerStorageInterface */ public function __construct(string $dsn) { - if (0 !== strpos($dsn, 'file:')) { + if (!str_starts_with($dsn, 'file:')) { throw new \RuntimeException(sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn)); } $this->folder = substr($dsn, 5); @@ -56,15 +56,15 @@ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, in } $file = fopen($file, 'r'); - fseek($file, 0, SEEK_END); + fseek($file, 0, \SEEK_END); $result = []; while (\count($result) < $limit && $line = $this->readLineFromFile($file)) { $values = str_getcsv($line); - list($csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode) = $values; + [$csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode] = $values; $csvTime = (int) $csvTime; - if ($ip && false === strpos($csvIp, $ip) || $url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) { + if ($ip && !str_contains($csvIp, $ip) || $url && !str_contains($csvUrl, $url) || $method && !str_contains($csvMethod, $method) || $statusCode && !str_contains($csvStatusCode, $statusCode)) { continue; } @@ -115,15 +115,7 @@ public function purge() */ public function read(string $token): ?Profile { - if (!$token || !file_exists($file = $this->getFilename($token))) { - return null; - } - - if (\function_exists('gzcompress')) { - $file = 'compress.zlib://'.$file; - } - - return $this->createProfileFromData($token, unserialize(file_get_contents($file))); + return $this->doRead($token); } /** @@ -165,14 +157,13 @@ public function write(Profile $profile): bool 'status_code' => $profile->getStatusCode(), ]; - $context = stream_context_create(); + $data = serialize($data); - if (\function_exists('gzcompress')) { - $file = 'compress.zlib://'.$file; - stream_context_set_option($context, 'zlib', 'level', 3); + if (\function_exists('gzencode')) { + $data = gzencode($data, 3); } - if (false === file_put_contents($file, serialize($data), 0, $context)) { + if (false === file_put_contents($file, $data, \LOCK_EX)) { return false; } @@ -200,7 +191,7 @@ public function write(Profile $profile): bool /** * Gets filename to store data, associated to the token. * - * @return string The profile filename + * @return string */ protected function getFilename(string $token) { @@ -214,7 +205,7 @@ protected function getFilename(string $token) /** * Gets the index filename. * - * @return string The index filename + * @return string */ protected function getIndexFilename() { @@ -228,7 +219,7 @@ protected function getIndexFilename() * * @param resource $file The file resource, with the pointer placed at the end of the line to read * - * @return mixed A string representing the line or null if beginning of file is reached + * @return mixed */ protected function readLineFromFile($file) { @@ -258,7 +249,7 @@ protected function readLineFromFile($file) $position += $upTo; $line = substr($buffer, $upTo + 1).$line; - fseek($file, max(0, $position), SEEK_SET); + fseek($file, max(0, $position), \SEEK_SET); if ('' !== $line) { break; @@ -287,17 +278,34 @@ protected function createProfileFromData(string $token, array $data, Profile $pa } foreach ($data['children'] as $token) { - if (!$token || !file_exists($file = $this->getFilename($token))) { - continue; + if (null !== $childProfile = $this->doRead($token, $profile)) { + $profile->addChild($childProfile); } + } - if (\function_exists('gzcompress')) { - $file = 'compress.zlib://'.$file; - } + return $profile; + } - $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile)); + private function doRead($token, Profile $profile = null): ?Profile + { + if (!$token || !file_exists($file = $this->getFilename($token))) { + return null; } - return $profile; + $h = fopen($file, 'r'); + flock($h, \LOCK_SH); + $data = stream_get_contents($h); + flock($h, \LOCK_UN); + fclose($h); + + if (\function_exists('gzdecode')) { + $data = @gzdecode($data) ?: $data; + } + + if (!$data = unserialize($data)) { + return null; + } + + return $this->createProfileFromData($token, $data, $profile); } } diff --git a/Profiler/Profile.php b/Profiler/Profile.php index 37b690ed86..a622403e1b 100644 --- a/Profiler/Profile.php +++ b/Profiler/Profile.php @@ -56,7 +56,7 @@ public function setToken(string $token) /** * Gets the token. * - * @return string The token + * @return string */ public function getToken() { @@ -74,7 +74,7 @@ public function setParent(self $parent) /** * Returns the parent profile. * - * @return self + * @return self|null */ public function getParent() { @@ -84,7 +84,7 @@ public function getParent() /** * Returns the parent token. * - * @return string|null The parent token + * @return string|null */ public function getParentToken() { @@ -94,7 +94,7 @@ public function getParentToken() /** * Returns the IP. * - * @return string|null The IP + * @return string|null */ public function getIp() { @@ -109,7 +109,7 @@ public function setIp(?string $ip) /** * Returns the request method. * - * @return string|null The request method + * @return string|null */ public function getMethod() { @@ -124,7 +124,7 @@ public function setMethod(string $method) /** * Returns the URL. * - * @return string|null The URL + * @return string|null */ public function getUrl() { @@ -137,15 +137,11 @@ public function setUrl(?string $url) } /** - * @return int The time + * @return int */ public function getTime() { - if (null === $this->time) { - return 0; - } - - return $this->time; + return $this->time ?? 0; } public function setTime(int $time) @@ -212,7 +208,7 @@ public function getChildByToken(string $token): ?self /** * Gets a Collector by name. * - * @return DataCollectorInterface A DataCollectorInterface instance + * @return DataCollectorInterface * * @throws \InvalidArgumentException if the collector does not exist */ diff --git a/Profiler/Profiler.php b/Profiler/Profiler.php index 72af8e0092..25e126f731 100644 --- a/Profiler/Profiler.php +++ b/Profiler/Profiler.php @@ -63,7 +63,7 @@ public function enable() /** * Loads the Profile for the given Response. * - * @return Profile|null A Profile instance + * @return Profile|null */ public function loadProfileFromResponse(Response $response) { @@ -77,7 +77,7 @@ public function loadProfileFromResponse(Response $response) /** * Loads the Profile for the given token. * - * @return Profile|null A Profile instance + * @return Profile|null */ public function loadProfile(string $token) { @@ -120,7 +120,7 @@ public function purge() * @param string|null $start The start date to search from * @param string|null $end The end date to search to * - * @return array An array of tokens + * @return array * * @see https://php.net/datetime.formats for the supported date/time formats */ @@ -132,7 +132,7 @@ public function find(?string $ip, ?string $url, ?string $limit, ?string $method, /** * Collects data for the given Response. * - * @return Profile|null A Profile instance or null if the profiler is disabled + * @return Profile|null */ public function collect(Request $request, Response $response, \Throwable $exception = null) { @@ -178,7 +178,7 @@ public function reset() /** * Gets the Collectors associated with this profiler. * - * @return array An array of collectors + * @return array */ public function all() { @@ -223,7 +223,7 @@ public function has(string $name) * * @param string $name A collector name * - * @return DataCollectorInterface A DataCollectorInterface instance + * @return DataCollectorInterface * * @throws \InvalidArgumentException if the collector does not exist */ diff --git a/Profiler/ProfilerStorageInterface.php b/Profiler/ProfilerStorageInterface.php index 0fbc44973e..95d72f46b3 100644 --- a/Profiler/ProfilerStorageInterface.php +++ b/Profiler/ProfilerStorageInterface.php @@ -32,8 +32,6 @@ interface ProfilerStorageInterface * @param int|null $limit The maximum number of tokens to return * @param int|null $start The start date to search from * @param int|null $end The end date to search to - * - * @return array An array of tokens */ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, int $start = null, int $end = null): array; @@ -41,15 +39,11 @@ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, in * Reads data associated with the given token. * * The method returns false if the token does not exist in the storage. - * - * @return Profile|null The profile associated with token */ public function read(string $token): ?Profile; /** * Saves a Profile. - * - * @return bool Write operation successful */ public function write(Profile $profile): bool; diff --git a/README.md b/README.md index abdaf513f9..ca50417827 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,29 @@ HttpKernel Component The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher component. It's flexible -enough to create a full-stack framework (Symfony), a micro-framework (Silex) or -an advanced CMS system (Drupal). +enough to create full-stack frameworks, micro-frameworks or advanced CMS systems like Drupal. + +Sponsor +------- + +The HttpKernel component for Symfony 5.4/6.0 is [backed][1] by [Les-Tilleuls.coop][2]. + +Les-Tilleuls.coop is a team of 50+ Symfony experts who can help you design, develop and +fix your projects. We provide a wide range of professional services including development, +consulting, coaching, training and audits. We also are highly skilled in JS, Go and DevOps. +We are a worker cooperative! + +Help Symfony by [sponsoring][3] its development! Resources --------- - * [Documentation](https://symfony.com/doc/current/components/http_kernel.html) - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + * [Documentation](https://symfony.com/doc/current/components/http_kernel.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) + +[1]: https://symfony.com/backers +[2]: https://les-tilleuls.coop +[3]: https://symfony.com/sponsor diff --git a/RebootableInterface.php b/RebootableInterface.php index 0dc46c3cef..e257237da9 100644 --- a/RebootableInterface.php +++ b/RebootableInterface.php @@ -21,10 +21,10 @@ interface RebootableInterface /** * Reboots a kernel. * - * The getCacheDir() method of a rebootable kernel should not be called - * while building the container. Use the %kernel.cache_dir% parameter instead. + * The getBuildDir() method of a rebootable kernel should not be called + * while building the container. Use the %kernel.build_dir% parameter instead. * - * @param string|null $warmupDir pass null to reboot in the regular cache directory + * @param string|null $warmupDir pass null to reboot in the regular build directory */ public function reboot(?string $warmupDir); } diff --git a/Resources/welcome.html.php b/Resources/welcome.html.php index b8337dc737..b25f99b3d0 100644 --- a/Resources/welcome.html.php +++ b/Resources/welcome.html.php @@ -19,8 +19,9 @@ .wrapper { text-align: center; width: 100%; } .container { position: relative; background: radial-gradient(ellipse at bottom, 0%, hsl(, 20%, 13%) 100%); background-attachment: fixed; color: ; } .container:after { content: ""; position: absolute; height: 2px; width: 2px; top: -2px; left: 0; background: white; box-shadow: 778px 1019px 0 0 rgba(255, 255, 255, 0.826) , 1075px 1688px 0 0 rgba(255,255,255, 0.275) , 388px 1021px 0 0 rgba(255,255,255, 0.259) , 1238px 626px 0 0 rgba(255,255,255, 0.469) , 997px 904px 0 0 rgba(255,255,255, 0.925) , 921px 1345px 0 0 rgba(255,255,255, 0.698) , 337px 1236px 0 0 rgba(255,255,255, 0.838) , 460px 569px 0 0 rgba(255,255,255, 0.01) , 690px 1488px 0 0 rgba(255,255,255, 0.154) , 859px 926px 0 0 rgba(255,255,255, 0.515) , 1272px 791px 0 0 rgba(255,255,255, 1) , 238px 1256px 0 0 rgba(255,255,255, 0.633) , 1486px 897px 0 0 rgba(255,255,255, 0.88) , 667px 6px 0 0 rgba(255,255,255, 0.508) , 853px 504px 0 0 rgba(255,255,255, 0.248) , 1329px 1778px 0 0 rgba(255,255,255, 0.217) , 768px 1340px 0 0 rgba(255,255,255, 0.792) , 631px 1383px 0 0 rgba(255,255,255, 0.698) , 991px 1603px 0 0 rgba(255,255,255, 0.939) , 1778px 1767px 0 0 rgba(255,255,255, 0.784) , 285px 546px 0 0 rgba(255,255,255, 0.8) , 1224px 1333px 0 0 rgba(255,255,255, 0.676) , 1154px 397px 0 0 rgba(255,255,255, 0.974) , 1210px 1004px 0 0 rgba(255,255,255, 0.894) , 1632px 953px 0 0 rgba(255,255,255, 0.281) , 449px 1144px 0 0 rgba(255,255,255, 0.706) , 1426px 771px 0 0 rgba(255,255,255, 0.737) , 1438px 1634px 0 0 rgba(255,255,255, 0.984) , 806px 168px 0 0 rgba(255,255,255, 0.807) , 731px 1067px 0 0 rgba(255,255,255, 0.734) , 1731px 1785px 0 0 rgba(255,255,255, 0.528) , 23px 975px 0 0 rgba(255,255,255, 0.068) , 575px 1088px 0 0 rgba(255,255,255, 0.876) , 1205px 1668px 0 0 rgba(255,255,255, 0.601) , 18px 1457px 0 0 rgba(255,255,255, 0.176) , 252px 1163px 0 0 rgba(255,255,255, 0.416) , 1752px 1px 0 0 rgba(255,255,255, 0.374) , 382px 767px 0 0 rgba(255,255,255, 0.073) , 133px 1462px 0 0 rgba(255,255,255, 0.706) , 851px 1166px 0 0 rgba(255,255,255, 0.535) , 374px 921px 0 0 rgba(255,255,255, 0.548) , 554px 1598px 0 0 rgba(255,255,255, 0.062) , 314px 685px 0 0 rgba(255,255,255, 0.187) , 1443px 209px 0 0 rgba(255,255,255, 0.097) , 1774px 1625px 0 0 rgba(255,255,255, 0.32) , 58px 278px 0 0 rgba(255,255,255, 0.684) , 986px 338px 0 0 rgba(255,255,255, 0.272) , 718px 1357px 0 0 rgba(255,255,255, 0.317) , 722px 983px 0 0 rgba(255,255,255, 0.568) , 1124px 992px 0 0 rgba(255,255,255, 0.199) , 581px 619px 0 0 rgba(255,255,255, 0.44) , 1120px 285px 0 0 rgba(255,255,255, 0.425) , 702px 138px 0 0 rgba(255,255,255, 0.816) , 262px 767px 0 0 rgba(255,255,255, 0.92) , 1204px 38px 0 0 rgba(255,255,255, 0.197) , 1196px 410px 0 0 rgba(255,255,255, 0.453) , 707px 699px 0 0 rgba(255,255,255, 0.481) , 1590px 1488px 0 0 rgba(255,255,255, 0.559) , 879px 1763px 0 0 rgba(255,255,255, 0.241) , 106px 686px 0 0 rgba(255,255,255, 0.175) , 158px 569px 0 0 rgba(255,255,255, 0.549) , 711px 1219px 0 0 rgba(255,255,255, 0.476) , 1339px 53px 0 0 rgba(255,255,255, 0.275) , 1410px 172px 0 0 rgba(255,255,255, 0.449) , 1601px 1484px 0 0 rgba(255,255,255, 0.988) , 1328px 1752px 0 0 rgba(255,255,255, 0.827) , 1733px 1475px 0 0 rgba(255,255,255, 0.567) , 559px 742px 0 0 rgba(255,255,255, 0.423) , 772px 844px 0 0 rgba(255,255,255, 0.039) , 602px 520px 0 0 rgba(255,255,255, 0.284) , 1158px 1067px 0 0 rgba(255,255,255, 0.066) , 1562px 730px 0 0 rgba(255,255,255, 0.086) , 1792px 615px 0 0 rgba(255,255,255, 0.438) , 1085px 1191px 0 0 rgba(255,255,255, 0.157) , 1402px 1087px 0 0 rgba(255,255,255, 0.797) , 569px 1685px 0 0 rgba(255,255,255, 0.992) , 1608px 52px 0 0 rgba(255,255,255, 0.302) , 1697px 1246px 0 0 rgba(255,255,255, 0.295) , 899px 1490px 0 0 rgba(255,255,255, 0.73) , 993px 901px 0 0 rgba(255,255,255, 0.961) , 1193px 1023px 0 0 rgba(255,255,255, 0.671) , 1224px 176px 0 0 rgba(255,255,255, 0.786) , 721px 1308px 0 0 rgba(255,255,255, 0.691) , 1702px 730px 0 0 rgba(255,255,255, 0.841) , 1480px 1498px 0 0 rgba(255,255,255, 0.655) , 181px 1612px 0 0 rgba(255,255,255, 0.588) , 1776px 679px 0 0 rgba(255,255,255, 0.821) , 892px 706px 0 0 rgba(255,255,255, 0.056) , 859px 267px 0 0 rgba(255,255,255, 0.565) , 784px 1285px 0 0 rgba(255,255,255, 0.029) , 1561px 1198px 0 0 rgba(255,255,255, 0.315) , 205px 421px 0 0 rgba(255,255,255, 0.584) , 236px 406px 0 0 rgba(255,255,255, 0.166) , 1259px 689px 0 0 rgba(255,255,255, 0.321) , 448px 317px 0 0 rgba(255,255,255, 0.495) , 1318px 466px 0 0 rgba(255,255,255, 0.275) , 1053px 297px 0 0 rgba(255,255,255, 0.035) , 716px 538px 0 0 rgba(255,255,255, 0.764) , 381px 207px 0 0 rgba(255,255,255, 0.692) , 871px 1140px 0 0 rgba(255,255,255, 0.342) , 361px 53px 0 0 rgba(255,255,255, 0.984) , 1565px 1593px 0 0 rgba(255,255,255, 0.102) , 145px 277px 0 0 rgba(255,255,255, 0.866) , 220px 1503px 0 0 rgba(255,255,255, 0.936) , 1068px 1475px 0 0 rgba(255,255,255, 0.156) , 1548px 483px 0 0 rgba(255,255,255, 0.768) , 710px 103px 0 0 rgba(255,255,255, 0.809) , 1660px 921px 0 0 rgba(255,255,255, 0.952) , 462px 1252px 0 0 rgba(255,255,255, 0.825) , 1123px 1628px 0 0 rgba(255,255,255, 0.409) , 1274px 729px 0 0 rgba(255,255,255, 0.26) , 1739px 679px 0 0 rgba(255,255,255, 0.83) , 1550px 1518px 0 0 rgba(255,255,255, 0.25) , 1624px 346px 0 0 rgba(255,255,255, 0.557) , 1023px 579px 0 0 rgba(255,255,255, 0.854) , 217px 661px 0 0 rgba(255,255,255, 0.731) , 1504px 549px 0 0 rgba(255,255,255, 0.705) , 939px 5px 0 0 rgba(255,255,255, 0.389) , 284px 735px 0 0 rgba(255,255,255, 0.355) , 13px 1679px 0 0 rgba(255,255,255, 0.712) , 137px 1592px 0 0 rgba(255,255,255, 0.619) , 1113px 505px 0 0 rgba(255,255,255, 0.651) , 1584px 510px 0 0 rgba(255,255,255, 0.41) , 346px 913px 0 0 rgba(255,255,255, 0.09) , 198px 1490px 0 0 rgba(255,255,255, 0.103) , 447px 1128px 0 0 rgba(255,255,255, 0.314) , 1356px 324px 0 0 rgba(255,255,255, 0.324) , 648px 667px 0 0 rgba(255,255,255, 0.155) , 442px 260px 0 0 rgba(255,255,255, 0.22) , 210px 401px 0 0 rgba(255,255,255, 0.682) , 422px 1772px 0 0 rgba(255,255,255, 0.671) , 276px 349px 0 0 rgba(255,255,255, 0.683) , 131px 539px 0 0 rgba(255,255,255, 0.977) , 892px 94px 0 0 rgba(255,255,255, 0.081) , 1295px 222px 0 0 rgba(255,255,255, 0.961) , 5px 1727px 0 0 rgba(255,255,255, 0.311) , 714px 1148px 0 0 rgba(255,255,255, 0.846) , 1455px 1182px 0 0 rgba(255,255,255, 0.313) , 1370px 708px 0 0 rgba(255,255,255, 0.824) , 812px 433px 0 0 rgba(255,255,255, 0.75) , 1110px 558px 0 0 rgba(255,255,255, 0.709) , 1132px 1543px 0 0 rgba(255,255,255, 0.868) , 644px 610px 0 0 rgba(255,255,255, 0.166) , 269px 1481px 0 0 rgba(255,255,255, 0.889) , 1712px 590px 0 0 rgba(255,255,255, 0.139) , 1159px 599px 0 0 rgba(255,255,255, 0.992) , 1551px 209px 0 0 rgba(255,255,255, 0.033) , 1020px 1721px 0 0 rgba(255,255,255, 0.028) , 216px 373px 0 0 rgba(255,255,255, 0.665) , 877px 532px 0 0 rgba(255,255,255, 0.686) , 1326px 885px 0 0 rgba(255,255,255, 0.517) , 972px 1704px 0 0 rgba(255,255,255, 0.499) , 749px 181px 0 0 rgba(255,255,255, 0.712) , 1511px 1650px 0 0 rgba(255,255,255, 0.101) , 1432px 183px 0 0 rgba(255,255,255, 0.545) , 1541px 1338px 0 0 rgba(255,255,255, 0.71) , 513px 1406px 0 0 rgba(255,255,255, 0.17) , 1314px 1197px 0 0 rgba(255,255,255, 0.789) , 824px 1659px 0 0 rgba(255,255,255, 0.597) , 308px 298px 0 0 rgba(255,255,255, 0.917) , 1225px 659px 0 0 rgba(255,255,255, 0.229) , 1253px 257px 0 0 rgba(255,255,255, 0.631) , 1653px 185px 0 0 rgba(255,255,255, 0.113) , 336px 614px 0 0 rgba(255,255,255, 0.045) , 1093px 898px 0 0 rgba(255,255,255, 0.617) , 730px 5px 0 0 rgba(255,255,255, 0.11) , 785px 645px 0 0 rgba(255,255,255, 0.516) , 989px 678px 0 0 rgba(255,255,255, 0.917) , 1511px 1614px 0 0 rgba(255,255,255, 0.938) , 584px 1117px 0 0 rgba(255,255,255, 0.631) , 534px 1012px 0 0 rgba(255,255,255, 0.668) , 1325px 1778px 0 0 rgba(255,255,255, 0.293) , 1632px 754px 0 0 rgba(255,255,255, 0.26) , 78px 1258px 0 0 rgba(255,255,255, 0.52) , 779px 1691px 0 0 rgba(255,255,255, 0.878) , 253px 1706px 0 0 rgba(255,255,255, 0.75) , 1358px 245px 0 0 rgba(255,255,255, 0.027) , 361px 1629px 0 0 rgba(255,255,255, 0.238) , 1134px 232px 0 0 rgba(255,255,255, 0.387) , 1685px 777px 0 0 rgba(255,255,255, 0.156) , 515px 724px 0 0 rgba(255,255,255, 0.863) , 588px 1728px 0 0 rgba(255,255,255, 0.159) , 1132px 47px 0 0 rgba(255,255,255, 0.691) , 315px 1446px 0 0 rgba(255,255,255, 0.782) , 79px 233px 0 0 rgba(255,255,255, 0.317) , 1498px 1050px 0 0 rgba(255,255,255, 0.358) , 30px 1073px 0 0 rgba(255,255,255, 0.939) , 1637px 620px 0 0 rgba(255,255,255, 0.141) , 1736px 1683px 0 0 rgba(255,255,255, 0.682) , 1298px 1505px 0 0 rgba(255,255,255, 0.863) , 972px 85px 0 0 rgba(255,255,255, 0.941) , 349px 1356px 0 0 rgba(255,255,255, 0.672) , 1545px 1429px 0 0 rgba(255,255,255, 0.859) , 1076px 467px 0 0 rgba(255,255,255, 0.024) , 189px 1647px 0 0 rgba(255,255,255, 0.838) , 423px 1722px 0 0 rgba(255,255,255, 0.771) , 1691px 1719px 0 0 rgba(255,255,255, 0.676) , 1747px 658px 0 0 rgba(255,255,255, 0.255) , 149px 1492px 0 0 rgba(255,255,255, 0.911) , 1203px 1138px 0 0 rgba(255,255,255, 0.964) , 781px 1584px 0 0 rgba(255,255,255, 0.465) , 1609px 1595px 0 0 rgba(255,255,255, 0.688) , 447px 1655px 0 0 rgba(255,255,255, 0.166) , 914px 1153px 0 0 rgba(255,255,255, 0.085) , 600px 1058px 0 0 rgba(255,255,255, 0.821) , 804px 505px 0 0 rgba(255,255,255, 0.608) , 1506px 584px 0 0 rgba(255,255,255, 0.618) , 587px 1290px 0 0 rgba(255,255,255, 0.071) , 258px 600px 0 0 rgba(255,255,255, 0.243) , 328px 395px 0 0 rgba(255,255,255, 0.065) , 846px 783px 0 0 rgba(255,255,255, 0.995) , 1138px 1294px 0 0 rgba(255,255,255, 0.703) , 1668px 633px 0 0 rgba(255,255,255, 0.27) , 337px 103px 0 0 rgba(255,255,255, 0.202) , 132px 986px 0 0 rgba(255,255,255, 0.726) , 414px 757px 0 0 rgba(255,255,255, 0.752) , 8px 1311px 0 0 rgba(255,255,255, 0.307) , 1791px 910px 0 0 rgba(255,255,255, 0.346) , 844px 216px 0 0 rgba(255,255,255, 0.156) , 1547px 1723px 0 0 rgba(255,255,255, 0.73) , 1187px 398px 0 0 rgba(255,255,255, 0.698) , 1550px 1520px 0 0 rgba(255,255,255, 0.462) , 1346px 655px 0 0 rgba(255,255,255, 0.58) , 668px 770px 0 0 rgba(255,255,255, 0.422) , 1774px 1435px 0 0 rgba(255,255,255, 0.089) , 693px 1061px 0 0 rgba(255,255,255, 0.893) , 132px 1689px 0 0 rgba(255,255,255, 0.937) , 894px 1561px 0 0 rgba(255,255,255, 0.88) , 906px 1706px 0 0 rgba(255,255,255, 0.567) , 1140px 297px 0 0 rgba(255,255,255, 0.358) , 13px 1288px 0 0 rgba(255,255,255, 0.464) , 1744px 423px 0 0 rgba(255,255,255, 0.845) , 119px 1548px 0 0 rgba(255,255,255, 0.769) , 1249px 1321px 0 0 rgba(255,255,255, 0.29) , 123px 795px 0 0 rgba(255,255,255, 0.597) , 390px 1542px 0 0 rgba(255,255,255, 0.47) , 825px 667px 0 0 rgba(255,255,255, 0.049) , 1071px 875px 0 0 rgba(255,255,255, 0.06) , 1428px 1786px 0 0 rgba(255,255,255, 0.222) , 993px 696px 0 0 rgba(255,255,255, 0.399) , 1585px 247px 0 0 rgba(255,255,255, 0.094) , 1340px 1312px 0 0 rgba(255,255,255, 0.603) , 1640px 725px 0 0 rgba(255,255,255, 0.026) , 1161px 1397px 0 0 rgba(255,255,255, 0.222) , 966px 1132px 0 0 rgba(255,255,255, 0.69) , 1782px 1275px 0 0 rgba(255,255,255, 0.606) , 1117px 1533px 0 0 rgba(255,255,255, 0.248) , 1027px 959px 0 0 rgba(255,255,255, 0.46) , 459px 839px 0 0 rgba(255,255,255, 0.98) , 1192px 265px 0 0 rgba(255,255,255, 0.523) , 175px 501px 0 0 rgba(255,255,255, 0.371) , 626px 19px 0 0 rgba(255,255,255, 0.246) , 46px 1173px 0 0 rgba(255,255,255, 0.124) , 573px 925px 0 0 rgba(255,255,255, 0.621) , 1px 283px 0 0 rgba(255,255,255, 0.943) , 778px 1213px 0 0 rgba(255,255,255, 0.128) , 435px 593px 0 0 rgba(255,255,255, 0.378) , 32px 394px 0 0 rgba(255,255,255, 0.451) , 1019px 1055px 0 0 rgba(255,255,255, 0.685) , 1423px 1233px 0 0 rgba(255,255,255, 0.354) , 494px 841px 0 0 rgba(255,255,255, 0.322) , 667px 194px 0 0 rgba(255,255,255, 0.655) , 1671px 195px 0 0 rgba(255,255,255, 0.502) , 403px 1710px 0 0 rgba(255,255,255, 0.623) , 665px 1597px 0 0 rgba(255,255,255, 0.839) , 61px 1742px 0 0 rgba(255,255,255, 0.566) , 1490px 1654px 0 0 rgba(255,255,255, 0.646) , 1361px 1604px 0 0 rgba(255,255,255, 0.101) , 1191px 1023px 0 0 rgba(255,255,255, 0.881) , 550px 378px 0 0 rgba(255,255,255, 0.573) , 1332px 1234px 0 0 rgba(255,255,255, 0.922) , 760px 1205px 0 0 rgba(255,255,255, 0.992) , 1506px 1328px 0 0 rgba(255,255,255, 0.723) , 1126px 813px 0 0 rgba(255,255,255, 0.549) , 67px 240px 0 0 rgba(255,255,255, 0.901) , 125px 1301px 0 0 rgba(255,255,255, 0.464) , 643px 391px 0 0 rgba(255,255,255, 0.589) , 1114px 1756px 0 0 rgba(255,255,255, 0.321) , 1602px 699px 0 0 rgba(255,255,255, 0.274) , 510px 393px 0 0 rgba(255,255,255, 0.185) , 171px 1217px 0 0 rgba(255,255,255, 0.932) , 1202px 1362px 0 0 rgba(255,255,255, 0.726) , 1160px 1324px 0 0 rgba(255,255,255, 0.867) , 121px 319px 0 0 rgba(255,255,255, 0.992) , 1474px 835px 0 0 rgba(255,255,255, 0.89) , 357px 1213px 0 0 rgba(255,255,255, 0.91) , 783px 976px 0 0 rgba(255,255,255, 0.941) , 750px 1599px 0 0 rgba(255,255,255, 0.515) , 323px 450px 0 0 rgba(255,255,255, 0.966) , 1078px 282px 0 0 rgba(255,255,255, 0.947) , 1164px 46px 0 0 rgba(255,255,255, 0.296) , 1792px 705px 0 0 rgba(255,255,255, 0.485) , 880px 1287px 0 0 rgba(255,255,255, 0.894) , 60px 1402px 0 0 rgba(255,255,255, 0.816) , 752px 894px 0 0 rgba(255,255,255, 0.803) , 285px 1535px 0 0 rgba(255,255,255, 0.93) , 1528px 401px 0 0 rgba(255,255,255, 0.727) , 651px 1767px 0 0 rgba(255,255,255, 0.146) , 1498px 1190px 0 0 rgba(255,255,255, 0.042) , 394px 1786px 0 0 rgba(255,255,255, 0.159) , 1318px 9px 0 0 rgba(255,255,255, 0.575) , 1699px 1675px 0 0 rgba(255,255,255, 0.511) , 82px 986px 0 0 rgba(255,255,255, 0.906) , 940px 970px 0 0 rgba(255,255,255, 0.562) , 1624px 259px 0 0 rgba(255,255,255, 0.537) , 1782px 222px 0 0 rgba(255,255,255, 0.259) , 1572px 1725px 0 0 rgba(255,255,255, 0.716) , 1080px 1557px 0 0 rgba(255,255,255, 0.245) , 1727px 648px 0 0 rgba(255,255,255, 0.471) , 899px 231px 0 0 rgba(255,255,255, 0.445) , 1061px 1074px 0 0 rgba(255,255,255, 0.079) , 556px 478px 0 0 rgba(255,255,255, 0.524) , 343px 359px 0 0 rgba(255,255,255, 0.162) , 711px 1254px 0 0 rgba(255,255,255, 0.323) , 1335px 242px 0 0 rgba(255,255,255, 0.936) , 933px 39px 0 0 rgba(255,255,255, 0.784) , 1629px 908px 0 0 rgba(255,255,255, 0.289) , 1800px 229px 0 0 rgba(255,255,255, 0.399) , 1589px 926px 0 0 rgba(255,255,255, 0.709) , 976px 694px 0 0 rgba(255,255,255, 0.855) , 1163px 1240px 0 0 rgba(255,255,255, 0.754) , 1662px 1784px 0 0 rgba(255,255,255, 0.088) , 656px 1388px 0 0 rgba(255,255,255, 0.688) , 1190px 1100px 0 0 rgba(255,255,255, 0.769) , 33px 392px 0 0 rgba(255,255,255, 0.301) , 56px 1405px 0 0 rgba(255,255,255, 0.969) , 1491px 118px 0 0 rgba(255,255,255, 0.991) , 1216px 997px 0 0 rgba(255,255,255, 0.727) , 1617px 712px 0 0 rgba(255,255,255, 0.45) , 163px 553px 0 0 rgba(255,255,255, 0.977) , 103px 140px 0 0 rgba(255,255,255, 0.916) , 1099px 1404px 0 0 rgba(255,255,255, 0.167) , 1423px 587px 0 0 rgba(255,255,255, 0.792) , 1797px 309px 0 0 rgba(255,255,255, 0.526) , 381px 141px 0 0 rgba(255,255,255, 0.005) , 1214px 802px 0 0 rgba(255,255,255, 0.887) , 211px 829px 0 0 rgba(255,255,255, 0.72) , 1103px 1507px 0 0 rgba(255,255,255, 0.642) , 244px 1231px 0 0 rgba(255,255,255, 0.184) , 118px 1747px 0 0 rgba(255,255,255, 0.475) , 183px 1293px 0 0 rgba(255,255,255, 0.148) , 911px 1362px 0 0 rgba(255,255,255, 0.073) , 817px 457px 0 0 rgba(255,255,255, 0.459) , 756px 18px 0 0 rgba(255,255,255, 0.544) , 481px 1118px 0 0 rgba(255,255,255, 0.878) , 380px 138px 0 0 rgba(255,255,255, 0.132) , 320px 646px 0 0 rgba(255,255,255, 0.04) , 1724px 1716px 0 0 rgba(255,255,255, 0.381) , 978px 1269px 0 0 rgba(255,255,255, 0.431) , 1530px 255px 0 0 rgba(255,255,255, 0.31) , 664px 204px 0 0 rgba(255,255,255, 0.913) , 474px 703px 0 0 rgba(255,255,255, 0.832) , 1722px 1204px 0 0 rgba(255,255,255, 0.356) , 1453px 821px 0 0 rgba(255,255,255, 0.195) , 730px 1468px 0 0 rgba(255,255,255, 0.696) , 928px 1610px 0 0 rgba(255,255,255, 0.894) , 1036px 304px 0 0 rgba(255,255,255, 0.696) , 1590px 172px 0 0 rgba(255,255,255, 0.729) , 249px 1590px 0 0 rgba(255,255,255, 0.277) , 357px 81px 0 0 rgba(255,255,255, 0.526) , 726px 1261px 0 0 rgba(255,255,255, 0.149) , 643px 946px 0 0 rgba(255,255,255, 0.005) , 1263px 995px 0 0 rgba(255,255,255, 0.124) , 1564px 1107px 0 0 rgba(255,255,255, 0.789) , 388px 83px 0 0 rgba(255,255,255, 0.498) , 715px 681px 0 0 rgba(255,255,255, 0.655) , 1618px 1624px 0 0 rgba(255,255,255, 0.63) , 1423px 1576px 0 0 rgba(255,255,255, 0.52) , 564px 1786px 0 0 rgba(255,255,255, 0.482) , 1066px 735px 0 0 rgba(255,255,255, 0.276) , 714px 1179px 0 0 rgba(255,255,255, 0.395) , 967px 1006px 0 0 rgba(255,255,255, 0.923) , 1136px 1790px 0 0 rgba(255,255,255, 0.801) , 215px 1690px 0 0 rgba(255,255,255, 0.957) , 1500px 1338px 0 0 rgba(255,255,255, 0.541) , 1679px 1065px 0 0 rgba(255,255,255, 0.925) , 426px 1489px 0 0 rgba(255,255,255, 0.193) , 1273px 853px 0 0 rgba(255,255,255, 0.317) , 665px 1189px 0 0 rgba(255,255,255, 0.512) , 520px 552px 0 0 rgba(255,255,255, 0.925) , 253px 438px 0 0 rgba(255,255,255, 0.588) , 369px 1354px 0 0 rgba(255,255,255, 0.889) , 749px 205px 0 0 rgba(255,255,255, 0.243) , 820px 145px 0 0 rgba(255,255,255, 0.207) , 1739px 228px 0 0 rgba(255,255,255, 0.267) , 392px 495px 0 0 rgba(255,255,255, 0.504) , 721px 1044px 0 0 rgba(255,255,255, 0.823) , 833px 912px 0 0 rgba(255,255,255, 0.222) , 865px 1499px 0 0 rgba(255,255,255, 0.003) , 313px 756px 0 0 rgba(255,255,255, 0.727) , 439px 1187px 0 0 rgba(255,255,255, 0.572) , 6px 1238px 0 0 rgba(255,255,255, 0.676) , 1567px 11px 0 0 rgba(255,255,255, 0.701) , 1216px 757px 0 0 rgba(255,255,255, 0.87) , 916px 588px 0 0 rgba(255,255,255, 0.565) , 831px 215px 0 0 rgba(255,255,255, 0.597) , 1289px 697px 0 0 rgba(255,255,255, 0.964) , 307px 34px 0 0 rgba(255,255,255, 0.462) , 3px 1685px 0 0 rgba(255,255,255, 0.464) , 1115px 1421px 0 0 rgba(255,255,255, 0.303) , 1451px 473px 0 0 rgba(255,255,255, 0.142) , 1374px 1205px 0 0 rgba(255,255,255, 0.086) , 1564px 317px 0 0 rgba(255,255,255, 0.773) , 304px 1127px 0 0 rgba(255,255,255, 0.653) , 446px 214px 0 0 rgba(255,255,255, 0.135) , 1541px 459px 0 0 rgba(255,255,255, 0.725) , 1387px 880px 0 0 rgba(255,255,255, 0.157) , 1172px 224px 0 0 rgba(255,255,255, 0.088) , 1420px 637px 0 0 rgba(255,255,255, 0.916) , 1385px 932px 0 0 rgba(255,255,255, 0.225) , 174px 1472px 0 0 rgba(255,255,255, 0.649) , 252px 750px 0 0 rgba(255,255,255, 0.277) , 825px 1042px 0 0 rgba(255,255,255, 0.707) , 840px 703px 0 0 rgba(255,255,255, 0.948) , 1478px 1800px 0 0 rgba(255,255,255, 0.151) , 95px 1303px 0 0 rgba(255,255,255, 0.332) , 1198px 740px 0 0 rgba(255,255,255, 0.443) , 141px 312px 0 0 rgba(255,255,255, 0.04) , 291px 729px 0 0 rgba(255,255,255, 0.284) , 1209px 1506px 0 0 rgba(255,255,255, 0.741) , 1188px 307px 0 0 rgba(255,255,255, 0.141) , 958px 41px 0 0 rgba(255,255,255, 0.858) , 1311px 1484px 0 0 rgba(255,255,255, 0.097) , 846px 1153px 0 0 rgba(255,255,255, 0.862) , 1238px 1376px 0 0 rgba(255,255,255, 0.071) , 1499px 342px 0 0 rgba(255,255,255, 0.719) , 640px 833px 0 0 rgba(255,255,255, 0.966) , 712px 545px 0 0 rgba(255,255,255, 0.194) , 1655px 1542px 0 0 rgba(255,255,255, 0.82) , 616px 353px 0 0 rgba(255,255,255, 0.871) , 1591px 1631px 0 0 rgba(255,255,255, 0.61) , 1664px 591px 0 0 rgba(255,255,255, 0.35) , 934px 454px 0 0 rgba(255,255,255, 0.58) , 1175px 477px 0 0 rgba(255,255,255, 0.966) , 299px 914px 0 0 rgba(255,255,255, 0.839) , 534px 243px 0 0 rgba(255,255,255, 0.194) , 773px 1135px 0 0 rgba(255,255,255, 0.42) , 1696px 1472px 0 0 rgba(255,255,255, 0.552) , 125px 523px 0 0 rgba(255,255,255, 0.591) , 1195px 382px 0 0 rgba(255,255,255, 0.904) , 1609px 1374px 0 0 rgba(255,255,255, 0.579) , 843px 82px 0 0 rgba(255,255,255, 0.072) , 1604px 451px 0 0 rgba(255,255,255, 0.545) , 1322px 190px 0 0 rgba(255,255,255, 0.034) , 528px 228px 0 0 rgba(255,255,255, 0.146) , 1470px 1169px 0 0 rgba(255,255,255, 0.912) , 502px 1350px 0 0 rgba(255,255,255, 0.594) , 1031px 298px 0 0 rgba(255,255,255, 0.368) , 1100px 1427px 0 0 rgba(255,255,255, 0.79) , 979px 1105px 0 0 rgba(255,255,255, 0.973) , 643px 1184px 0 0 rgba(255,255,255, 0.813) , 1636px 1701px 0 0 rgba(255,255,255, 0.013) , 1004px 245px 0 0 rgba(255,255,255, 0.412) , 680px 740px 0 0 rgba(255,255,255, 0.967) , 1599px 562px 0 0 rgba(255,255,255, 0.66) , 256px 1617px 0 0 rgba(255,255,255, 0.463) , 314px 1092px 0 0 rgba(255,255,255, 0.734) , 870px 900px 0 0 rgba(255,255,255, 0.512) , 530px 60px 0 0 rgba(255,255,255, 0.198) , 1786px 896px 0 0 rgba(255,255,255, 0.392) , 636px 212px 0 0 rgba(255,255,255, 0.997) , 672px 540px 0 0 rgba(255,255,255, 0.632) , 1118px 1649px 0 0 rgba(255,255,255, 0.377) , 433px 647px 0 0 rgba(255,255,255, 0.902) , 1200px 1737px 0 0 rgba(255,255,255, 0.262) , 1258px 143px 0 0 rgba(255,255,255, 0.729) , 1603px 1364px 0 0 rgba(255,255,255, 0.192) , 66px 1756px 0 0 rgba(255,255,255, 0.681) , 946px 263px 0 0 rgba(255,255,255, 0.105) , 1216px 1082px 0 0 rgba(255,255,255, 0.287) , 6px 1143px 0 0 rgba(255,255,255, 0.017) , 1631px 126px 0 0 rgba(255,255,255, 0.449) , 357px 1565px 0 0 rgba(255,255,255, 0.163) , 1752px 261px 0 0 rgba(255,255,255, 0.423) , 1247px 1631px 0 0 rgba(255,255,255, 0.312) , 320px 671px 0 0 rgba(255,255,255, 0.695) , 1375px 596px 0 0 rgba(255,255,255, 0.856) , 1456px 1340px 0 0 rgba(255,255,255, 0.564) , 447px 1044px 0 0 rgba(255,255,255, 0.623) , 1732px 447px 0 0 rgba(255,255,255, 0.216) , 174px 1509px 0 0 rgba(255,255,255, 0.398) , 16px 861px 0 0 rgba(255,255,255, 0.904) , 878px 1296px 0 0 rgba(255,255,255, 0.205) , 1725px 1483px 0 0 rgba(255,255,255, 0.704) , 255px 48px 0 0 rgba(255,255,255, 0.7) , 610px 1669px 0 0 rgba(255,255,255, 0.865) , 1044px 1251px 0 0 rgba(255,255,255, 0.98) , 884px 862px 0 0 rgba(255,255,255, 0.198) , 986px 545px 0 0 rgba(255,255,255, 0.379) , 1620px 217px 0 0 rgba(255,255,255, 0.159) , 383px 1763px 0 0 rgba(255,255,255, 0.518) , 595px 974px 0 0 rgba(255,255,255, 0.347) , 359px 14px 0 0 rgba(255,255,255, 0.863) , 95px 1385px 0 0 rgba(255,255,255, 0.011) , 411px 1030px 0 0 rgba(255,255,255, 0.038) , 345px 789px 0 0 rgba(255,255,255, 0.771) , 421px 460px 0 0 rgba(255,255,255, 0.133) , 972px 1160px 0 0 rgba(255,255,255, 0.342) , 597px 1061px 0 0 rgba(255,255,255, 0.781) , 1017px 1092px 0 0 rgba(255,255,255, 0.437); } - .warning { background: ; display: flex; align-content: center; padding: 10px; text-align: left; justify-content: center; } - .warning svg { height: 48px; width: 48px; margin-right: 10px; } + .warning { background: ; display: flex; align-items: center; padding: 10px; text-align: left; justify-content: center; } + .warning svg { flex-shrink: 0; height: 32px; width: 32px; margin-right: 10px; } + .warning p { line-height: 1.4; margin: 0; } .container svg.wave { position: absolute; bottom: -2px; left: 0; z-index: 1; } .container .logo { margin-bottom: 1em; } .container .logo svg { fill: hsl(, 20%, 26%); } @@ -45,9 +46,6 @@ @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } .sf-toolbar { opacity: 0; -webkit-animation: fade-in 1s .2s forwards; animation: fade-in 1s .2s forwards; z-index: 99999; } - body { font-size: 20px; } - .warning { text-align: center; } - .warning svg { height: 32px; width: 32px; } .resources .row { margin-left: 50px; margin-right: 50px; } .resource { padding: 0 30px; } @@ -59,13 +57,19 @@ .resource h2 { font-size: 22px; } .resource a { font-size: 16px; margin-top: 0; } } + @media (min-width: 992px) { + body { font-size: 20px; } + .warning { text-align: center; } + }
- You're seeing this page because you haven't configured any homepage URL and debug mode is enabled. +

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

diff --git a/Tests/Bundle/BundleTest.php b/Tests/Bundle/BundleTest.php index be03734dc4..0937eebcc4 100644 --- a/Tests/Bundle/BundleTest.php +++ b/Tests/Bundle/BundleTest.php @@ -30,7 +30,7 @@ public function testGetContainerExtension() public function testGetContainerExtensionWithInvalidClass() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface'); $bundle = new ExtensionNotValidBundle(); $bundle->getContainerExtension(); diff --git a/Tests/CacheClearer/ChainCacheClearerTest.php b/Tests/CacheClearer/ChainCacheClearerTest.php index b97559e321..80d9796070 100644 --- a/Tests/CacheClearer/ChainCacheClearerTest.php +++ b/Tests/CacheClearer/ChainCacheClearerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\CacheClearer; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer; class ChainCacheClearerTest extends TestCase @@ -30,7 +31,7 @@ public static function tearDownAfterClass(): void public function testInjectClearersInConstructor() { - $clearer = $this->getMockClearer(); + $clearer = $this->createMock(CacheClearerInterface::class); $clearer ->expects($this->once()) ->method('clear'); @@ -38,9 +39,4 @@ public function testInjectClearersInConstructor() $chainClearer = new ChainCacheClearer([$clearer]); $chainClearer->clear(self::$cacheDir); } - - protected function getMockClearer() - { - return $this->getMockBuilder('Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface')->getMock(); - } } diff --git a/Tests/CacheClearer/Psr6CacheClearerTest.php b/Tests/CacheClearer/Psr6CacheClearerTest.php index 6e0a47e930..26b323f810 100644 --- a/Tests/CacheClearer/Psr6CacheClearerTest.php +++ b/Tests/CacheClearer/Psr6CacheClearerTest.php @@ -19,7 +19,7 @@ class Psr6CacheClearerTest extends TestCase { public function testClearPoolsInjectedInConstructor() { - $pool = $this->getMockBuilder(CacheItemPoolInterface::class)->getMock(); + $pool = $this->createMock(CacheItemPoolInterface::class); $pool ->expects($this->once()) ->method('clear'); @@ -29,17 +29,19 @@ public function testClearPoolsInjectedInConstructor() public function testClearPool() { - $pool = $this->getMockBuilder(CacheItemPoolInterface::class)->getMock(); + $pool = $this->createMock(CacheItemPoolInterface::class); $pool ->expects($this->once()) - ->method('clear'); + ->method('clear') + ->willReturn(true) + ; - (new Psr6CacheClearer(['pool' => $pool]))->clearPool('pool'); + $this->assertTrue((new Psr6CacheClearer(['pool' => $pool]))->clearPool('pool')); } public function testClearPoolThrowsExceptionOnUnreferencedPool() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Cache pool not found: "unknown"'); (new Psr6CacheClearer())->clearPool('unknown'); } diff --git a/Tests/CacheWarmer/CacheWarmerAggregateTest.php b/Tests/CacheWarmer/CacheWarmerAggregateTest.php index 4266c0a182..c34cc9c400 100644 --- a/Tests/CacheWarmer/CacheWarmerAggregateTest.php +++ b/Tests/CacheWarmer/CacheWarmerAggregateTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; class CacheWarmerAggregateTest extends TestCase { @@ -30,7 +31,7 @@ public static function tearDownAfterClass(): void public function testInjectWarmersUsingConstructor() { - $warmer = $this->getCacheWarmerMock(); + $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->once()) ->method('warmUp'); @@ -40,7 +41,7 @@ public function testInjectWarmersUsingConstructor() public function testWarmupDoesCallWarmupOnOptionalWarmersWhenEnableOptionalWarmersIsEnabled() { - $warmer = $this->getCacheWarmerMock(); + $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->never()) ->method('isOptional'); @@ -55,7 +56,7 @@ public function testWarmupDoesCallWarmupOnOptionalWarmersWhenEnableOptionalWarme public function testWarmupDoesNotCallWarmupOnOptionalWarmersWhenEnableOptionalWarmersIsNotEnabled() { - $warmer = $this->getCacheWarmerMock(); + $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->once()) ->method('isOptional') @@ -67,13 +68,4 @@ public function testWarmupDoesNotCallWarmupOnOptionalWarmersWhenEnableOptionalWa $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->warmUp(self::$cacheDir); } - - protected function getCacheWarmerMock() - { - $warmer = $this->getMockBuilder('Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface') - ->disableOriginalConstructor() - ->getMock(); - - return $warmer; - } } diff --git a/Tests/CacheWarmer/CacheWarmerTest.php b/Tests/CacheWarmer/CacheWarmerTest.php index eeb39e4dca..02d07fe1bc 100644 --- a/Tests/CacheWarmer/CacheWarmerTest.php +++ b/Tests/CacheWarmer/CacheWarmerTest.php @@ -38,7 +38,7 @@ public function testWriteCacheFileCreatesTheFile() public function testWriteNonWritableCacheFileThrowsARuntimeException() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $nonWritableFile = '/this/file/is/very/probably/not/writable'; $warmer = new TestCacheWarmer($nonWritableFile); $warmer->warmUp(\dirname($nonWritableFile)); @@ -57,7 +57,7 @@ public function __construct(string $file) /** * @return string[] */ - public function warmUp(string $cacheDir) + public function warmUp(string $cacheDir): array { $this->writeCacheFile($this->file, 'content'); diff --git a/Tests/Config/FileLocatorTest.php b/Tests/Config/FileLocatorTest.php index 9f1d093b52..6108e62135 100644 --- a/Tests/Config/FileLocatorTest.php +++ b/Tests/Config/FileLocatorTest.php @@ -13,12 +13,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Config\FileLocator; +use Symfony\Component\HttpKernel\KernelInterface; class FileLocatorTest extends TestCase { public function testLocate() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->atLeastOnce()) ->method('locateResource') @@ -30,7 +31,7 @@ public function testLocate() $kernel ->expects($this->never()) ->method('locateResource'); - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $locator->locate('/some/path'); } } diff --git a/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php b/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php index 4f85b0f351..4577b5a6d2 100644 --- a/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\NotTaggedControllerValueResolver; @@ -55,7 +56,7 @@ public function testDoNotSupportEmptyController() public function testController() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); @@ -66,7 +67,7 @@ public function testController() public function testControllerWithATrailingBackSlash() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); @@ -77,7 +78,7 @@ public function testControllerWithATrailingBackSlash() public function testControllerWithMethodNameStartUppercase() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); @@ -88,7 +89,7 @@ public function testControllerWithMethodNameStartUppercase() public function testControllerNameIsAnArray() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); @@ -97,6 +98,17 @@ public function testControllerNameIsAnArray() $resolver->resolve($request, $argument); } + public function testInvokableController() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::__invoke()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); + $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); + $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); + $request = $this->requestWithAttributes(['_controller' => 'App\Controller\Mine']); + $this->assertTrue($resolver->supports($request, $argument)); + $resolver->resolve($request, $argument); + } + private function requestWithAttributes(array $attributes) { $request = Request::create('/'); diff --git a/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php b/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php index 4036727bce..69b9511c05 100644 --- a/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php +++ b/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; @@ -107,7 +108,7 @@ public function testControllerNameIsAnArray() public function testErrorIsTruncated() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Cannot autowire argument $dummy of "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.'); $container = new ContainerBuilder(); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); diff --git a/Tests/Controller/ArgumentResolverTest.php b/Tests/Controller/ArgumentResolverTest.php index 449bd1ae94..42daae2378 100644 --- a/Tests/Controller/ArgumentResolverTest.php +++ b/Tests/Controller/ArgumentResolverTest.php @@ -132,7 +132,7 @@ public function testGetArgumentsFailsOnUnresolvedValue() self::$resolver->getArguments($request, $controller); $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); } catch (\Exception $e) { - $this->assertInstanceOf('\RuntimeException', $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + $this->assertInstanceOf(\RuntimeException::class, $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); } } @@ -164,7 +164,7 @@ public function testGetVariadicArguments() public function testGetVariadicArgumentsWithoutArrayInRequest() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'foo'); @@ -175,9 +175,9 @@ public function testGetVariadicArgumentsWithoutArrayInRequest() public function testGetArgumentWithoutArray() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $factory = new ArgumentMetadataFactory(); - $valueResolver = $this->getMockBuilder(ArgumentValueResolverInterface::class)->getMock(); + $valueResolver = $this->createMock(ArgumentValueResolverInterface::class); $resolver = new ArgumentResolver($factory, [$valueResolver]); $valueResolver->expects($this->any())->method('supports')->willReturn(true); @@ -192,7 +192,7 @@ public function testGetArgumentWithoutArray() public function testIfExceptionIsThrownWhenMissingAnArgument() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $request = Request::create('/'); $controller = [$this, 'controllerWithFoo']; @@ -241,7 +241,7 @@ public function testGetSessionArgumentsWithExtendedSession() public function testGetSessionArgumentsWithInterface() { - $session = $this->getMockBuilder(SessionInterface::class)->getMock(); + $session = $this->createMock(SessionInterface::class); $request = Request::create('/'); $request->setSession($session); $controller = [$this, 'controllerWithSessionInterface']; @@ -251,8 +251,8 @@ public function testGetSessionArgumentsWithInterface() public function testGetSessionMissMatchWithInterface() { - $this->expectException('RuntimeException'); - $session = $this->getMockBuilder(SessionInterface::class)->getMock(); + $this->expectException(\RuntimeException::class); + $session = $this->createMock(SessionInterface::class); $request = Request::create('/'); $request->setSession($session); $controller = [$this, 'controllerWithExtendingSession']; @@ -262,7 +262,7 @@ public function testGetSessionMissMatchWithInterface() public function testGetSessionMissMatchWithImplementation() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $session = new Session(new MockArraySessionStorage()); $request = Request::create('/'); $request->setSession($session); @@ -273,7 +273,7 @@ public function testGetSessionMissMatchWithImplementation() public function testGetSessionMissMatchOnNull() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $request = Request::create('/'); $controller = [$this, 'controllerWithExtendingSession']; diff --git a/Tests/Controller/ContainerControllerResolverTest.php b/Tests/Controller/ContainerControllerResolverTest.php index d394c3bce4..9ce7ec467c 100644 --- a/Tests/Controller/ContainerControllerResolverTest.php +++ b/Tests/Controller/ContainerControllerResolverTest.php @@ -158,9 +158,9 @@ public function getControllers() public function testExceptionWhenUsingRemovedControllerServiceWithClassNameAsName() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?'); - $container = $this->getMockBuilder(Container::class)->getMock(); + $container = $this->createMock(Container::class); $container->expects($this->once()) ->method('has') ->with(ControllerTestService::class) @@ -182,9 +182,9 @@ public function testExceptionWhenUsingRemovedControllerServiceWithClassNameAsNam public function testExceptionWhenUsingRemovedControllerService() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Controller "app.my_controller" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?'); - $container = $this->getMockBuilder(Container::class)->getMock(); + $container = $this->createMock(Container::class); $container->expects($this->once()) ->method('has') ->with('app.my_controller') @@ -239,7 +239,7 @@ protected function createControllerResolver(LoggerInterface $logger = null, Cont protected function createMockContainer() { - return $this->getMockBuilder(ContainerInterface::class)->getMock(); + return $this->createMock(ContainerInterface::class); } } diff --git a/Tests/Controller/ControllerResolverTest.php b/Tests/Controller/ControllerResolverTest.php index d5ac4ad5c2..2afcfe4b4e 100644 --- a/Tests/Controller/ControllerResolverTest.php +++ b/Tests/Controller/ControllerResolverTest.php @@ -20,7 +20,7 @@ class ControllerResolverTest extends TestCase { public function testGetControllerWithoutControllerParameter() { - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once())->method('warning')->with('Unable to look for the controller as the "_controller" parameter is missing.'); $resolver = $this->createControllerResolver($logger); @@ -94,7 +94,7 @@ public function testGetControllerWithInvokableClass() public function testGetControllerOnObjectWithoutInvokeMethod() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $resolver = $this->createControllerResolver(); $request = Request::create('/'); @@ -170,9 +170,9 @@ public function getUndefinedControllers() $controller = new ControllerTest(); return [ - ['foo', \Error::class, 'Class \'foo\' not found'], - ['oof::bar', \Error::class, 'Class \'oof\' not found'], - [['oof', 'bar'], \Error::class, 'Class \'oof\' not found'], + ['foo', \Error::class, \PHP_VERSION_ID < 80000 ? 'Class \'foo\' not found' : 'Class "foo" not found'], + ['oof::bar', \Error::class, \PHP_VERSION_ID < 80000 ? 'Class \'oof\' not found' : 'Class "oof" not found'], + [['oof', 'bar'], \Error::class, \PHP_VERSION_ID < 80000 ? 'Class \'oof\' not found' : 'Class "oof" not found'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], diff --git a/Tests/Controller/ErrorControllerTest.php b/Tests/Controller/ErrorControllerTest.php index fadd820ea6..1b3a833578 100644 --- a/Tests/Controller/ErrorControllerTest.php +++ b/Tests/Controller/ErrorControllerTest.php @@ -27,7 +27,7 @@ class ErrorControllerTest extends TestCase */ public function testInvokeController(Request $request, \Exception $exception, int $statusCode, string $content) { - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $errorRenderer = new HtmlErrorRenderer(); $controller = new ErrorController($kernel, null, $errorRenderer); $response = $controller($exception); @@ -67,7 +67,7 @@ public function testPreviewController() $_controller = 'error_controller'; $code = 404; - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') diff --git a/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index dfab909802..6c3f2a1ea8 100644 --- a/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; @@ -105,6 +107,17 @@ public function testBasicTypesSignature() ], $arguments); } + public function testNamedClosure() + { + $arguments = $this->factory->createArgumentMetadata(\Closure::fromCallable([$this, 'signature1'])); + + $this->assertEquals([ + new ArgumentMetadata('foo', self::class, false, false, null), + new ArgumentMetadata('bar', 'array', false, false, null), + new ArgumentMetadata('baz', 'callable', false, false, null), + ], $arguments); + } + public function testNullableTypesSignature() { $arguments = $this->factory->createArgumentMetadata([new NullableController(), 'action']); @@ -117,6 +130,39 @@ public function testNullableTypesSignature() ], $arguments); } + /** + * @requires PHP 8 + */ + public function testAttributeSignature() + { + $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']); + + $this->assertEquals([ + new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]), + ], $arguments); + } + + /** + * @requires PHP 8 + */ + public function testMultipleAttributes() + { + $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg']); + $this->assertCount(1, $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg'])[0]->getAttributes()); + } + + /** + * @requires PHP 8 + */ + public function testIssue41478() + { + $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'issue41478']); + $this->assertEquals([ + new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]), + new ArgumentMetadata('bat', 'string', false, false, null, false, []), + ], $arguments); + } + private function signature1(self $foo, array $bar, callable $baz) { } diff --git a/Tests/ControllerMetadata/ArgumentMetadataTest.php b/Tests/ControllerMetadata/ArgumentMetadataTest.php index 5ce4b1f76b..45b15e1744 100644 --- a/Tests/ControllerMetadata/ArgumentMetadataTest.php +++ b/Tests/ControllerMetadata/ArgumentMetadataTest.php @@ -12,10 +12,15 @@ namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\HttpKernel\Attribute\ArgumentInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; class ArgumentMetadataTest extends TestCase { + use ExpectDeprecationTrait; + public function testWithBcLayerWithDefault() { $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value'); @@ -34,11 +39,34 @@ public function testDefaultValueAvailable() public function testDefaultValueUnavailable() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $argument = new ArgumentMetadata('foo', 'string', false, false, null, false); $this->assertFalse($argument->isNullable()); $this->assertFalse($argument->hasDefaultValue()); $argument->getDefaultValue(); } + + /** + * @group legacy + */ + public function testLegacyAttribute() + { + $attribute = $this->createMock(ArgumentInterface::class); + + $this->expectDeprecation('Since symfony/http-kernel 5.3: The "Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata" constructor expects an array of PHP attributes as last argument, %s given.'); + $this->expectDeprecation('Since symfony/http-kernel 5.3: Method "Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata::getAttribute()" is deprecated, use "getAttributes()" instead.'); + + $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, $attribute); + $this->assertSame($attribute, $argument->getAttribute()); + } + + /** + * @requires PHP 8 + */ + public function testGetAttributes() + { + $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new Foo('bar')]); + $this->assertEquals([new Foo('bar')], $argument->getAttributes()); + } } diff --git a/Tests/DataCollector/ConfigDataCollectorTest.php b/Tests/DataCollector/ConfigDataCollectorTest.php index 9cb75a6051..cbcf5d36eb 100644 --- a/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/Tests/DataCollector/ConfigDataCollectorTest.php @@ -30,17 +30,52 @@ public function testCollect() $this->assertSame('test', $c->getEnv()); $this->assertTrue($c->isDebug()); $this->assertSame('config', $c->getName()); - $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', PHP_VERSION); - $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', PHP_VERSION); + $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', \PHP_VERSION); + $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', \PHP_VERSION); $this->assertSame(\PHP_INT_SIZE * 8, $c->getPhpArchitecture()); - $this->assertSame(class_exists('Locale', false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); + $this->assertSame(class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); $this->assertSame(date_default_timezone_get(), $c->getPhpTimezone()); $this->assertSame(Kernel::VERSION, $c->getSymfonyVersion()); $this->assertSame(4 === Kernel::MINOR_VERSION, $c->isSymfonyLts()); $this->assertNull($c->getToken()); $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); - $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN), $c->hasZendOpcache()); - $this->assertSame(\extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN), $c->hasApcu()); + $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN), $c->hasZendOpcache()); + $this->assertSame(\extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN), $c->hasApcu()); + $this->assertSame(sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); + $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); + + $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); + $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->format('F Y'); + $this->assertSame($eom, $c->getSymfonyEom()); + $this->assertSame($eol, $c->getSymfonyEol()); + } + + public function testCollectWithoutKernel() + { + $c = new ConfigDataCollector(); + $c->collect(new Request(), new Response()); + + $this->assertSame('n/a', $c->getEnv()); + $this->assertSame('n/a', $c->isDebug()); + $this->assertSame('config', $c->getName()); + $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', \PHP_VERSION); + $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', \PHP_VERSION); + $this->assertSame(\PHP_INT_SIZE * 8, $c->getPhpArchitecture()); + $this->assertSame(class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); + $this->assertSame(date_default_timezone_get(), $c->getPhpTimezone()); + $this->assertSame(Kernel::VERSION, $c->getSymfonyVersion()); + $this->assertSame(4 === Kernel::MINOR_VERSION, $c->isSymfonyLts()); + $this->assertNull($c->getToken()); + $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); + $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN), $c->hasZendOpcache()); + $this->assertSame(\extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN), $c->hasApcu()); + $this->assertSame(sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); + $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); + + $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); + $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->format('F Y'); + $this->assertSame($eom, $c->getSymfonyEom()); + $this->assertSame($eol, $c->getSymfonyEol()); } } diff --git a/Tests/DataCollector/DumpDataCollectorTest.php b/Tests/DataCollector/DumpDataCollectorTest.php index a40a482278..b86e53df79 100644 --- a/Tests/DataCollector/DumpDataCollectorTest.php +++ b/Tests/DataCollector/DumpDataCollectorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Server\Connection; @@ -28,7 +29,7 @@ public function testDump() { $data = new Data([[123]]); - $collector = new DumpDataCollector(); + $collector = new DumpDataCollector(null, new FileLinkFormatter([])); $this->assertSame('dump', $collector->getName()); @@ -62,7 +63,7 @@ public function testDumpWithServerConnection() $data = new Data([[123]]); // Server is up, server dumper is used - $serverDumper = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); + $serverDumper = $this->createMock(Connection::class); $serverDumper->expects($this->once())->method('write')->willReturn(true); $collector = new DumpDataCollector(null, null, null, null, $serverDumper); diff --git a/Tests/DataCollector/LoggerDataCollectorTest.php b/Tests/DataCollector/LoggerDataCollectorTest.php index 9c175397fc..bf3fb65522 100644 --- a/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/Tests/DataCollector/LoggerDataCollectorTest.php @@ -18,13 +18,14 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; +use Symfony\Component\VarDumper\Cloner\Data; class LoggerDataCollectorTest extends TestCase { public function testCollectWithUnexpectedFormat() { $logger = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Log\DebugLoggerInterface') + ->getMockBuilder(DebugLoggerInterface::class) ->setMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->willReturn(123); @@ -45,11 +46,57 @@ public function testCollectWithUnexpectedFormat() ], $compilerLogs['Unknown Compiler Pass']); } - public function testWithMasterRequest() + public function testCollectFromDeprecationsLog() + { + $containerPathPrefix = __DIR__.'/'; + $path = $containerPathPrefix.'Deprecations.log'; + touch($path); + file_put_contents($path, serialize([[ + 'type' => 16384, + 'message' => 'The "Symfony\Bundle\FrameworkBundle\Controller\Controller" class is deprecated since Symfony 4.2, use Symfony\Bundle\FrameworkBundle\Controller\AbstractController instead.', + 'file' => '/home/hamza/projet/contrib/sf/vendor/symfony/framework-bundle/Controller/Controller.php', + 'line' => 17, + 'trace' => [[ + 'file' => '/home/hamza/projet/contrib/sf/src/Controller/DefaultController.php', + 'line' => 9, + 'function' => 'spl_autoload_call', + ]], + 'count' => 1, + ]])); + + $logger = $this + ->getMockBuilder(DebugLoggerInterface::class) + ->setMethods(['countErrors', 'getLogs', 'clear']) + ->getMock(); + + $logger->expects($this->once())->method('countErrors')->willReturn(0); + $logger->expects($this->exactly(2))->method('getLogs')->willReturn([]); + + $c = new LoggerDataCollector($logger, $containerPathPrefix); + $c->lateCollect(); + + $processedLogs = $c->getProcessedLogs(); + + $this->assertCount(1, $processedLogs); + + $this->assertEquals($processedLogs[0]['type'], 'deprecation'); + $this->assertEquals($processedLogs[0]['errorCount'], 1); + $this->assertEquals($processedLogs[0]['timestamp'], (new \DateTimeImmutable())->setTimestamp(filemtime($path))->format(\DateTimeInterface::RFC3339_EXTENDED)); + $this->assertEquals($processedLogs[0]['priority'], 100); + $this->assertEquals($processedLogs[0]['priorityName'], 'DEBUG'); + $this->assertNull($processedLogs[0]['channel']); + + $this->assertInstanceOf(Data::class, $processedLogs[0]['message']); + $this->assertInstanceOf(Data::class, $processedLogs[0]['context']); + + @unlink($path); + } + + public function testWithMainRequest() { - $masterRequest = new Request(); + $mainRequest = new Request(); $stack = new RequestStack(); - $stack->push($masterRequest); + $stack->push($mainRequest); $logger = $this ->getMockBuilder(DebugLoggerInterface::class) @@ -60,16 +107,16 @@ public function testWithMasterRequest() $c = new LoggerDataCollector($logger, __DIR__.'/', $stack); - $c->collect($masterRequest, new Response()); + $c->collect($mainRequest, new Response()); $c->lateCollect(); } public function testWithSubRequest() { - $masterRequest = new Request(); + $mainRequest = new Request(); $subRequest = new Request(); $stack = new RequestStack(); - $stack->push($masterRequest); + $stack->push($mainRequest); $stack->push($subRequest); $logger = $this @@ -91,7 +138,7 @@ public function testWithSubRequest() public function testCollect($nb, $logs, $expectedLogs, $expectedDeprecationCount, $expectedScreamCount, $expectedPriorities = null) { $logger = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Log\DebugLoggerInterface') + ->getMockBuilder(DebugLoggerInterface::class) ->setMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->willReturn($nb); @@ -123,7 +170,7 @@ public function testCollect($nb, $logs, $expectedLogs, $expectedDeprecationCount public function testReset() { $logger = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Log\DebugLoggerInterface') + ->getMockBuilder(DebugLoggerInterface::class) ->setMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('clear'); @@ -157,14 +204,14 @@ public function getCollectTestData() yield 'logs with some deprecations' => [ 1, [ - ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], - ['message' => 'foo', 'context' => ['exception' => new \ErrorException('deprecated', 0, E_DEPRECATED)], 'priority' => 100, 'priorityName' => 'DEBUG'], - ['message' => 'foo2', 'context' => ['exception' => new \ErrorException('deprecated', 0, E_USER_DEPRECATED)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, \E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo', 'context' => ['exception' => new \ErrorException('deprecated', 0, \E_DEPRECATED)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo2', 'context' => ['exception' => new \ErrorException('deprecated', 0, \E_USER_DEPRECATED)], 'priority' => 100, 'priorityName' => 'DEBUG'], ], [ - ['message' => 'foo3', 'context' => ['exception' => ['warning', E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], - ['message' => 'foo', 'context' => ['exception' => ['deprecated', E_DEPRECATED]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => false], - ['message' => 'foo2', 'context' => ['exception' => ['deprecated', E_USER_DEPRECATED]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => false], + ['message' => 'foo3', 'context' => ['exception' => ['warning', \E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo', 'context' => ['exception' => ['deprecated', \E_DEPRECATED]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => false], + ['message' => 'foo2', 'context' => ['exception' => ['deprecated', \E_USER_DEPRECATED]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => false], ], 2, 0, @@ -174,14 +221,14 @@ public function getCollectTestData() yield 'logs with some silent errors' => [ 1, [ - ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], - ['message' => 'foo3', 'context' => ['exception' => new SilencedErrorContext(E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], - ['message' => '0', 'context' => ['exception' => new SilencedErrorContext(E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, \E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo3', 'context' => ['exception' => new SilencedErrorContext(\E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => '0', 'context' => ['exception' => new SilencedErrorContext(\E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], ], [ - ['message' => 'foo3', 'context' => ['exception' => ['warning', E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], - ['message' => 'foo3', 'context' => ['exception' => [E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], - ['message' => '0', 'context' => ['exception' => [E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], + ['message' => 'foo3', 'context' => ['exception' => ['warning', \E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], + ['message' => 'foo3', 'context' => ['exception' => [\E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], + ['message' => '0', 'context' => ['exception' => [\E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], ], 0, 2, diff --git a/Tests/DataCollector/RequestDataCollectorTest.php b/Tests/DataCollector/RequestDataCollectorTest.php index a8a6bad61f..d7c8b302b6 100644 --- a/Tests/DataCollector/RequestDataCollectorTest.php +++ b/Tests/DataCollector/RequestDataCollectorTest.php @@ -17,10 +17,15 @@ use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; +use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; @@ -39,12 +44,12 @@ public function testCollect() $attributes = $c->getRequestAttributes(); $this->assertSame('request', $c->getName()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestHeaders()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestServer()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestCookies()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $attributes); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestRequest()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getRequestQuery()); + $this->assertInstanceOf(ParameterBag::class, $c->getRequestHeaders()); + $this->assertInstanceOf(ParameterBag::class, $c->getRequestServer()); + $this->assertInstanceOf(ParameterBag::class, $c->getRequestCookies()); + $this->assertInstanceOf(ParameterBag::class, $attributes); + $this->assertInstanceOf(ParameterBag::class, $c->getRequestRequest()); + $this->assertInstanceOf(ParameterBag::class, $c->getRequestQuery()); $this->assertInstanceOf(ParameterBag::class, $c->getResponseCookies()); $this->assertSame('html', $c->getFormat()); $this->assertEquals('foobar', $c->getRoute()); @@ -54,7 +59,7 @@ public function testCollect() $this->assertContainsEquals(__FILE__, $attributes->get('resource')); $this->assertSame('stdClass', $attributes->get('object')->getType()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\ParameterBag', $c->getResponseHeaders()); + $this->assertInstanceOf(ParameterBag::class, $c->getResponseHeaders()); $this->assertSame('OK', $c->getStatusText()); $this->assertSame(200, $c->getStatusCode()); $this->assertSame('application/json', $c->getContentType()); @@ -203,10 +208,10 @@ public function testItAddsRedirectedAttributesWhenRequestContainsSpecificCookie( 'sf_redirect' => '{}', ]); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $c = new RequestDataCollector(); - $c->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $this->createResponse())); + $c->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $this->createResponse())); $this->assertTrue($request->attributes->get('_redirected')); } @@ -248,6 +253,97 @@ public function testItCollectsTheRedirectionAndClearTheCookie() $this->assertNull($cookie->getValue()); } + public function testItCollectsTheSessionTraceProperly() + { + $collector = new RequestDataCollector(); + $request = $this->createRequest(); + + // RequestDataCollectorTest doesn't implement SessionInterface or SessionBagInterface, therefore should do nothing. + $collector->collectSessionUsage(); + + $collector->collect($request, $this->createResponse()); + $this->assertSame([], $collector->getSessionUsages()); + + $collector->reset(); + + $session = $this->createMock(SessionInterface::class); + $session->method('getMetadataBag')->willReturnCallback(static function () use ($collector) { + $collector->collectSessionUsage(); + + return new MetadataBag(); + }); + $session->getMetadataBag(); + + $collector->collect($request, $this->createResponse()); + $collector->lateCollect(); + + $usages = $collector->getSessionUsages(); + + $this->assertCount(1, $usages); + $this->assertSame(__FILE__, $usages[0]['file']); + $this->assertSame(__LINE__ - 9, $line = $usages[0]['line']); + + $trace = $usages[0]['trace']; + $this->assertSame('getMetadataBag', $trace[0]['function']); + $this->assertSame(self::class, $class = $trace[1]['class']); + + $this->assertSame(sprintf('%s:%s', $class, $line), $usages[0]['name']); + } + + public function testStatelessCheck() + { + $requestStack = new RequestStack(); + $request = $this->createRequest(); + $requestStack->push($request); + + $collector = new RequestDataCollector($requestStack); + $collector->collect($request, $response = $this->createResponse()); + $collector->lateCollect(); + + $this->assertFalse($collector->getStatelessCheck()); + + $requestStack = new RequestStack(); + $request = $this->createRequest(); + $request->attributes->set('_stateless', true); + $requestStack->push($request); + + $collector = new RequestDataCollector($requestStack); + $collector->collect($request, $response = $this->createResponse()); + $collector->lateCollect(); + + $this->assertTrue($collector->getStatelessCheck()); + + $requestStack = new RequestStack(); + $request = $this->createRequest(); + + $collector = new RequestDataCollector($requestStack); + $collector->collect($request, $response = $this->createResponse()); + $collector->lateCollect(); + + $this->assertFalse($collector->getStatelessCheck()); + } + + public function testItHidesPassword() + { + $c = new RequestDataCollector(); + + $request = Request::create( + 'http://test.com/login', + 'POST', + ['_password' => ' _password@123'], + [], + [], + [], + '_password=%20_password%40123' + ); + + $c->collect($request, $this->createResponse()); + $c->lateCollect(); + + $this->assertEquals('******', $c->getRequestRequest()->get('_password')); + $this->assertEquals('_password=******', $c->getContent()); + } + protected function createRequest($routeParams = ['name' => 'foo']) { $request = Request::create('http://test.com/foo?bar=baz'); @@ -288,9 +384,9 @@ protected function createResponse() */ protected function injectController($collector, $controller, $request) { - $resolver = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface')->getMock(); - $httpKernel = new HttpKernel(new EventDispatcher(), $resolver, null, $this->getMockBuilder(ArgumentResolverInterface::class)->getMock()); - $event = new ControllerEvent($httpKernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + $resolver = $this->createMock(ControllerResolverInterface::class); + $httpKernel = new HttpKernel(new EventDispatcher(), $resolver, null, $this->createMock(ArgumentResolverInterface::class)); + $event = new ControllerEvent($httpKernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST); $collector->onKernelController($event); } diff --git a/Tests/DataCollector/TimeDataCollectorTest.php b/Tests/DataCollector/TimeDataCollectorTest.php index 9de9eb599a..0496f1a3d1 100644 --- a/Tests/DataCollector/TimeDataCollectorTest.php +++ b/Tests/DataCollector/TimeDataCollectorTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Stopwatch\Stopwatch; /** @@ -43,7 +44,7 @@ public function testCollect() $c->collect($request, new Response()); $this->assertEquals(0, $c->getStartTime()); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once())->method('getStartTime')->willReturn(123456.0); $c = new TimeDataCollector($kernel); diff --git a/Tests/Debug/FileLinkFormatterTest.php b/Tests/Debug/FileLinkFormatterTest.php index 1f4d298bf3..4d801f2749 100644 --- a/Tests/Debug/FileLinkFormatterTest.php +++ b/Tests/Debug/FileLinkFormatterTest.php @@ -20,7 +20,7 @@ class FileLinkFormatterTest extends TestCase { public function testWhenNoFileLinkFormatAndNoRequest() { - $sut = new FileLinkFormatter(); + $sut = new FileLinkFormatter([]); $this->assertFalse($sut->format('/kernel/root/src/my/very/best/file.php', 3)); } @@ -47,8 +47,17 @@ public function testWhenNoFileLinkFormatAndRequest() $request->server->set('SCRIPT_FILENAME', '/public/index.php'); $request->server->set('REQUEST_URI', '/index.php/example'); - $sut = new FileLinkFormatter(null, $requestStack, __DIR__, '/_profiler/open?file=%f&line=%l#line%l'); + $sut = new FileLinkFormatter([], $requestStack, __DIR__, '/_profiler/open?file=%f&line=%l#line%l'); $this->assertSame('http://www.example.org/_profiler/open?file=file.php&line=3#line3', $sut->format($file, 3)); } + + public function testIdeFileLinkFormat() + { + $file = __DIR__.\DIRECTORY_SEPARATOR.'file.php'; + + $sut = new FileLinkFormatter('atom'); + + $this->assertSame("atom://core/open/file?filename=$file&line=3", $sut->format($file, 3)); + } } diff --git a/Tests/Debug/TraceableEventDispatcherTest.php b/Tests/Debug/TraceableEventDispatcherTest.php index 0fa6fcde51..a307bbff55 100644 --- a/Tests/Debug/TraceableEventDispatcherTest.php +++ b/Tests/Debug/TraceableEventDispatcherTest.php @@ -16,6 +16,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; +use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\Stopwatch\Stopwatch; @@ -26,12 +28,12 @@ class TraceableEventDispatcherTest extends TestCase public function testStopwatchSections() { $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch = new Stopwatch()); - $kernel = $this->getHttpKernel($dispatcher, function () { return new Response('', 200, ['X-Debug-Token' => '292e1e']); }); + $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $response = $kernel->handle($request); $kernel->terminate($request, $response); - $events = $stopwatch->getSectionEvents($response->headers->get('X-Debug-Token')); + $events = $stopwatch->getSectionEvents($request->attributes->get('_stopwatch_token')); $this->assertEquals([ '__section__', 'kernel.request', @@ -45,7 +47,7 @@ public function testStopwatchSections() public function testStopwatchCheckControllerOnRequestEvent() { - $stopwatch = $this->getMockBuilder('Symfony\Component\Stopwatch\Stopwatch') + $stopwatch = $this->getMockBuilder(Stopwatch::class) ->setMethods(['isStarted']) ->getMock(); $stopwatch->expects($this->once()) @@ -54,25 +56,25 @@ public function testStopwatchCheckControllerOnRequestEvent() $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch); - $kernel = $this->getHttpKernel($dispatcher, function () { return new Response(); }); + $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $kernel->handle($request); } public function testStopwatchStopControllerOnRequestEvent() { - $stopwatch = $this->getMockBuilder('Symfony\Component\Stopwatch\Stopwatch') + $stopwatch = $this->getMockBuilder(Stopwatch::class) ->setMethods(['isStarted', 'stop']) ->getMock(); $stopwatch->expects($this->once()) ->method('isStarted') ->willReturn(true); - $stopwatch->expects($this->once()) + $stopwatch->expects($this->exactly(3)) ->method('stop'); $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch); - $kernel = $this->getHttpKernel($dispatcher, function () { return new Response(); }); + $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $kernel->handle($request); } @@ -108,11 +110,13 @@ public function testListenerCanRemoveItselfWhenExecuted() $this->assertCount(1, $eventDispatcher->getListeners('foo'), 'expected listener1 to be removed'); } - protected function getHttpKernel($dispatcher, $controller) + protected function getHttpKernel($dispatcher) { - $controllerResolver = $this->getMockBuilder('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface')->getMock(); - $controllerResolver->expects($this->once())->method('getController')->willReturn($controller); - $argumentResolver = $this->getMockBuilder('Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface')->getMock(); + $controllerResolver = $this->createMock(ControllerResolverInterface::class); + $controllerResolver->expects($this->once())->method('getController')->willReturn(function () { + return new Response(); + }); + $argumentResolver = $this->createMock(ArgumentResolverInterface::class); $argumentResolver->expects($this->once())->method('getArguments')->willReturn([]); return new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver); diff --git a/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php b/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php index 2694d002cf..c95a7fb524 100644 --- a/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php +++ b/Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php @@ -39,7 +39,7 @@ public function testServicesAreOrderedAccordingToPriority() $container = new ContainerBuilder(); $container->setDefinition('argument_resolver', $definition); - foreach ($services as $id => list($tag)) { + foreach ($services as $id => [$tag]) { $container->register($id)->addTag('controller.argument_value_resolver', $tag); } @@ -72,7 +72,7 @@ public function testInDebugWithStopWatchDefinition() $container->register('debug.stopwatch', Stopwatch::class); $container->setDefinition('argument_resolver', $definition); - foreach ($services as $id => list($tag)) { + foreach ($services as $id => [$tag]) { $container->register($id)->addTag('controller.argument_value_resolver', $tag); } diff --git a/Tests/DependencyInjection/FragmentRendererPassTest.php b/Tests/DependencyInjection/FragmentRendererPassTest.php index 49567d40be..ab0efe32f5 100644 --- a/Tests/DependencyInjection/FragmentRendererPassTest.php +++ b/Tests/DependencyInjection/FragmentRendererPassTest.php @@ -29,7 +29,7 @@ class FragmentRendererPassTest extends TestCase */ public function testContentRendererWithoutInterface() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $builder = new ContainerBuilder(); $fragmentHandlerDefinition = $builder->register('fragment.handler'); $builder->register('my_content_renderer', 'Symfony\Component\DependencyInjection\Definition') diff --git a/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php b/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php index 39a1cb73b1..c8db5e55e9 100644 --- a/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php +++ b/Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php @@ -12,22 +12,25 @@ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler; +use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; class LazyLoadingFragmentHandlerTest extends TestCase { public function testRender() { - $renderer = $this->getMockBuilder('Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface')->getMock(); + $renderer = $this->createMock(FragmentRendererInterface::class); $renderer->expects($this->once())->method('getName')->willReturn('foo'); $renderer->expects($this->any())->method('render')->willReturn(new Response()); - $requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStack = $this->createMock(RequestStack::class); $requestStack->expects($this->any())->method('getCurrentRequest')->willReturn(Request::create('/')); - $container = $this->getMockBuilder('Psr\Container\ContainerInterface')->getMock(); + $container = $this->createMock(ContainerInterface::class); $container->expects($this->once())->method('has')->with('foo')->willReturn(true); $container->expects($this->once())->method('get')->willReturn($renderer); diff --git a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 2cda6c4f56..a8fdd7975b 100644 --- a/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -13,21 +13,25 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\TypedReference; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; +use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; class RegisterControllerArgumentLocatorsPassTest extends TestCase { public function testInvalidClass() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Class "Symfony\Component\HttpKernel\Tests\DependencyInjection\NotFound" used for service "foo" cannot be found.'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -42,7 +46,7 @@ public function testInvalidClass() public function testNoAction() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "action" attribute on tag "controller.service_arguments" {"argument":"bar"} for service "foo".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -57,7 +61,7 @@ public function testNoAction() public function testNoArgument() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "argument" attribute on tag "controller.service_arguments" {"action":"fooAction"} for service "foo".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -72,7 +76,7 @@ public function testNoArgument() public function testNoService() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "id" attribute on tag "controller.service_arguments" {"action":"fooAction","argument":"bar"} for service "foo".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -87,7 +91,7 @@ public function testNoService() public function testInvalidMethod() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid "action" attribute on tag "controller.service_arguments" for service "foo": no public "barAction()" method found on class "Symfony\Component\HttpKernel\Tests\DependencyInjection\RegisterTestController".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -102,7 +106,7 @@ public function testInvalidMethod() public function testInvalidArgument() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid "controller.service_arguments" tag for service "foo": method "fooAction()" has no "baz" argument on class "Symfony\Component\HttpKernel\Tests\DependencyInjection\RegisterTestController".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -197,7 +201,7 @@ public function testSkipSetContainer() public function testExceptionOnNonExistentTypeHint() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClass". Did you forget to add a use statement?'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -217,7 +221,7 @@ public function testExceptionOnNonExistentTypeHint() public function testExceptionOnNonExistentTypeHintDifferentNamespace() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassDifferentNamespaceController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Acme\NonExistentClass".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); @@ -247,7 +251,8 @@ public function testNoExceptionOnNonExistentTypeHintOptionalArg() $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $this->assertSame(['foo::barAction', 'foo::fooAction'], array_keys($locator)); + + $this->assertEqualsCanonicalizing(['foo::barAction', 'foo::fooAction'], array_keys($locator)); } public function testArgumentWithNoTypeHintIsOk() @@ -394,7 +399,68 @@ public function testAlias() $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $this->assertSame([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator)); + $this->assertEqualsCanonicalizing([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator)); + } + + /** + * @requires PHP 8.1 + */ + public function testEnumArgumentIsIgnored() + { + $container = new ContainerBuilder(); + $resolver = $container->register('argument_resolver.service')->addArgument([]); + + $container->register('foo', NonNullableEnumArgumentWithDefaultController::class) + ->addTag('controller.service_arguments') + ; + + $pass = new RegisterControllerArgumentLocatorsPass(); + $pass->process($container); + + $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $this->assertEmpty(array_keys($locator), 'enum typed argument is ignored'); + } + + /** + * @requires PHP 8 + */ + public function testBindWithTarget() + { + $container = new ContainerBuilder(); + $resolver = $container->register('argument_resolver.service')->addArgument([]); + + $container->register(ControllerDummy::class, 'bar'); + $container->register(ControllerDummy::class.' $imageStorage', 'baz'); + + $container->register('foo', WithTarget::class) + ->setBindings(['string $someApiKey' => new Reference('the_api_key')]) + ->addTag('controller.service_arguments'); + + (new RegisterControllerArgumentLocatorsPass())->process($container); + + $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + + $expected = [ + 'apiKey' => new ServiceClosureArgument(new Reference('the_api_key')), + 'service1' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'imageStorage')), + 'service2' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'service2')), + ]; + $this->assertEquals($expected, $locator->getArgument(0)); + } + + public function testResponseArgumentIsIgnored() + { + $container = new ContainerBuilder(); + $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]); + + $container->register('foo', WithResponseArgument::class) + ->addTag('controller.service_arguments'); + + (new RegisterControllerArgumentLocatorsPass())->process($container); + + $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $this->assertEmpty(array_keys($locator), 'Response typed argument is ignored'); } } @@ -457,3 +523,29 @@ public function fooAction(string $someArg) { } } + +class NonNullableEnumArgumentWithDefaultController +{ + public function fooAction(Suit $suit = Suit::Spades) + { + } +} + +class WithTarget +{ + public function fooAction( + #[Target('some.api.key')] + string $apiKey, + #[Target('image.storage')] + ControllerDummy $service1, + ControllerDummy $service2 + ) { + } +} + +class WithResponseArgument +{ + public function fooAction(Response $response, ?Response $nullableResponse) + { + } +} diff --git a/Tests/DependencyInjection/RegisterLocaleAwareServicesPassTest.php b/Tests/DependencyInjection/RegisterLocaleAwareServicesPassTest.php index aa3c6aa0c4..ffa8b0dd72 100644 --- a/Tests/DependencyInjection/RegisterLocaleAwareServicesPassTest.php +++ b/Tests/DependencyInjection/RegisterLocaleAwareServicesPassTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; diff --git a/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php b/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php index b5e55bdea9..b9dd84d592 100644 --- a/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php +++ b/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php @@ -57,7 +57,7 @@ public function testProcess() 'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing method "setTestCase" of service "c2" from controller candidates: the method is called at instantiation, thus cannot be an action.', ]; - $this->assertSame($expectedLog, $container->getCompiler()->getLog()); + $this->assertEqualsCanonicalizing($expectedLog, $container->getCompiler()->getLog()); } public function testInvoke() diff --git a/Tests/DependencyInjection/ResettableServicePassTest.php b/Tests/DependencyInjection/ResettableServicePassTest.php index 9dbc2b08a4..0e24c1d059 100644 --- a/Tests/DependencyInjection/ResettableServicePassTest.php +++ b/Tests/DependencyInjection/ResettableServicePassTest.php @@ -1,11 +1,21 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; @@ -57,7 +67,7 @@ public function testCompilerPass() public function testMissingMethod() { - $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Tag "kernel.reset" requires the "method" attribute to be set.'); $container = new ContainerBuilder(); $container->register(ResettableService::class) @@ -69,6 +79,26 @@ public function testMissingMethod() $container->compile(); } + public function testIgnoreInvalidMethod() + { + $container = new ContainerBuilder(); + $container->register(ResettableService::class) + ->setPublic(true) + ->addTag('kernel.reset', ['method' => 'missingMethod', 'on_invalid' => 'ignore']); + $container->register('services_resetter', ServicesResetter::class) + ->setPublic(true) + ->setArguments([null, []]); + $container->addCompilerPass(new ResettableServicePass()); + + $container->compile(); + + $this->assertSame([ResettableService::class => ['?missingMethod']], $container->getDefinition('services_resetter')->getArgument(1)); + + $resettable = $container->get(ResettableService::class); + $resetter = $container->get('services_resetter'); + $resetter->reset(); + } + public function testCompilerPassWithoutResetters() { $container = new ContainerBuilder(); diff --git a/Tests/Event/ControllerArgumentsEventTest.php b/Tests/Event/ControllerArgumentsEventTest.php index 7758a66667..87dab37a84 100644 --- a/Tests/Event/ControllerArgumentsEventTest.php +++ b/Tests/Event/ControllerArgumentsEventTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Event; use PHPUnit\Framework\TestCase; diff --git a/Tests/EventListener/AddRequestFormatsListenerTest.php b/Tests/EventListener/AddRequestFormatsListenerTest.php index da8dc6fb0b..c40468f3da 100644 --- a/Tests/EventListener/AddRequestFormatsListenerTest.php +++ b/Tests/EventListener/AddRequestFormatsListenerTest.php @@ -12,14 +12,14 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\AddRequestFormatsListener; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; /** - * Test AddRequestFormatsListener class. - * * @author Gildas Quemener */ class AddRequestFormatsListenerTest extends TestCase @@ -41,12 +41,12 @@ protected function tearDown(): void public function testIsAnEventSubscriber() { - $this->assertInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface', $this->listener); + $this->assertInstanceOf(EventSubscriberInterface::class, $this->listener); } public function testRegisteredEvent() { - $this->assertEquals( + $this->assertSame( [KernelEvents::REQUEST => ['onKernelRequest', 100]], AddRequestFormatsListener::getSubscribedEvents() ); @@ -54,8 +54,8 @@ public function testRegisteredEvent() public function testSetAdditionalFormats() { - $request = $this->getRequestMock(); - $event = $this->getRequestEventMock($request); + $request = $this->createMock(Request::class); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $request->expects($this->once()) ->method('setFormat') @@ -63,23 +63,4 @@ public function testSetAdditionalFormats() $this->listener->onKernelRequest($event); } - - protected function getRequestMock() - { - return $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); - } - - protected function getRequestEventMock(Request $request) - { - $event = $this - ->getMockBuilder(RequestEvent::class) - ->disableOriginalConstructor() - ->getMock(); - - $event->expects($this->any()) - ->method('getRequest') - ->willReturn($request); - - return $event; - } } diff --git a/Tests/EventListener/DebugHandlersListenerTest.php b/Tests/EventListener/DebugHandlersListenerTest.php index abd202f381..c8581927a8 100644 --- a/Tests/EventListener/DebugHandlersListenerTest.php +++ b/Tests/EventListener/DebugHandlersListenerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\ConsoleEvents; @@ -34,9 +35,11 @@ */ class DebugHandlersListenerTest extends TestCase { + use ExpectDeprecationTrait; + public function testConfigure() { - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $userHandler = function () {}; $listener = new DebugHandlersListener($userHandler, $logger); $eHandler = new ErrorHandler(); @@ -47,9 +50,10 @@ public function testConfigure() try { $listener->configure(); } catch (\Exception $exception) { + } finally { + restore_exception_handler(); + restore_error_handler(); } - restore_exception_handler(); - restore_error_handler(); if (null !== $exception) { throw $exception; @@ -59,8 +63,8 @@ public function testConfigure() $loggers = $eHandler->setLoggers([]); - $this->assertArrayHasKey(E_DEPRECATED, $loggers); - $this->assertSame([$logger, LogLevel::INFO], $loggers[E_DEPRECATED]); + $this->assertArrayHasKey(\E_DEPRECATED, $loggers); + $this->assertSame([$logger, LogLevel::INFO], $loggers[\E_DEPRECATED]); } public function testConfigureForHttpKernelWithNoTerminateWithException() @@ -68,9 +72,9 @@ public function testConfigureForHttpKernelWithNoTerminateWithException() $listener = new DebugHandlersListener(null); $eHandler = new ErrorHandler(); $event = new KernelEvent( - $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(), + $this->createMock(HttpKernelInterface::class), Request::create('/'), - HttpKernelInterface::MASTER_REQUEST + HttpKernelInterface::MAIN_REQUEST ); $exception = null; @@ -92,7 +96,7 @@ public function testConsoleEvent() { $dispatcher = new EventDispatcher(); $listener = new DebugHandlersListener(null); - $app = $this->getMockBuilder('Symfony\Component\Console\Application')->getMock(); + $app = $this->createMock(Application::class); $app->expects($this->once())->method('getHelperSet')->willReturn(new HelperSet()); $command = new Command(__FUNCTION__); $command->setApplication($app); @@ -113,16 +117,17 @@ public function testConsoleEvent() try { $dispatcher->dispatch($event, ConsoleEvents::COMMAND); } catch (\Exception $exception) { + } finally { + restore_exception_handler(); + restore_error_handler(); } - restore_exception_handler(); - restore_error_handler(); if (null !== $exception) { throw $exception; } $xHandler = $eHandler->setExceptionHandler('var_dump'); - $this->assertInstanceOf('Closure', $xHandler); + $this->assertInstanceOf(\Closure::class, $xHandler); $app->expects($this->once()) ->method(method_exists(Application::class, 'renderThrowable') ? 'renderThrowable' : 'renderException'); @@ -156,28 +161,28 @@ public function provideLevelsAssignedToLoggers(): array { return [ [false, false, '0', null, null], - [false, false, E_ALL, null, null], + [false, false, \E_ALL, null, null], [false, false, [], null, null], - [false, false, [E_WARNING => LogLevel::WARNING, E_USER_DEPRECATED => LogLevel::NOTICE], null, null], + [false, false, [\E_WARNING => LogLevel::WARNING, \E_USER_DEPRECATED => LogLevel::NOTICE], null, null], - [true, false, E_ALL, E_ALL, null], - [true, false, E_DEPRECATED, E_DEPRECATED, null], + [true, false, \E_ALL, \E_ALL, null], + [true, false, \E_DEPRECATED, \E_DEPRECATED, null], [true, false, [], null, null], - [true, false, [E_WARNING => LogLevel::WARNING, E_DEPRECATED => LogLevel::NOTICE], [E_WARNING => LogLevel::WARNING, E_DEPRECATED => LogLevel::NOTICE], null], + [true, false, [\E_WARNING => LogLevel::WARNING, \E_DEPRECATED => LogLevel::NOTICE], [\E_WARNING => LogLevel::WARNING, \E_DEPRECATED => LogLevel::NOTICE], null], [false, true, '0', null, null], - [false, true, E_ALL, null, E_DEPRECATED | E_USER_DEPRECATED], - [false, true, E_ERROR, null, null], + [false, true, \E_ALL, null, \E_DEPRECATED | \E_USER_DEPRECATED], + [false, true, \E_ERROR, null, null], [false, true, [], null, null], - [false, true, [E_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::DEBUG], null, [E_DEPRECATED => LogLevel::DEBUG]], + [false, true, [\E_ERROR => LogLevel::ERROR, \E_DEPRECATED => LogLevel::DEBUG], null, [\E_DEPRECATED => LogLevel::DEBUG]], [true, true, '0', null, null], - [true, true, E_ALL, E_ALL & ~(E_DEPRECATED | E_USER_DEPRECATED), E_DEPRECATED | E_USER_DEPRECATED], - [true, true, E_ERROR, E_ERROR, null], - [true, true, E_USER_DEPRECATED, null, E_USER_DEPRECATED], - [true, true, [E_ERROR => LogLevel::ERROR, E_DEPRECATED => LogLevel::DEBUG], [E_ERROR => LogLevel::ERROR], [E_DEPRECATED => LogLevel::DEBUG]], - [true, true, [E_ERROR => LogLevel::ALERT], [E_ERROR => LogLevel::ALERT], null], - [true, true, [E_USER_DEPRECATED => LogLevel::NOTICE], null, [E_USER_DEPRECATED => LogLevel::NOTICE]], + [true, true, \E_ALL, \E_ALL & ~(\E_DEPRECATED | \E_USER_DEPRECATED), \E_DEPRECATED | \E_USER_DEPRECATED], + [true, true, \E_ERROR, \E_ERROR, null], + [true, true, \E_USER_DEPRECATED, null, \E_USER_DEPRECATED], + [true, true, [\E_ERROR => LogLevel::ERROR, \E_DEPRECATED => LogLevel::DEBUG], [\E_ERROR => LogLevel::ERROR], [\E_DEPRECATED => LogLevel::DEBUG]], + [true, true, [\E_ERROR => LogLevel::ALERT], [\E_ERROR => LogLevel::ALERT], null], + [true, true, [\E_USER_DEPRECATED => LogLevel::NOTICE], null, [\E_USER_DEPRECATED => LogLevel::NOTICE]], ]; } @@ -219,7 +224,7 @@ public function testLevelsAssignedToLoggers(bool $hasLogger, bool $hasDeprecatio ->method('setDefaultLogger') ->withConsecutive(...$expectedCalls); - $sut = new DebugHandlersListener(null, $logger, $levels, null, true, null, true, $deprecationLogger); + $sut = new DebugHandlersListener(null, $logger, $levels, null, true, true, $deprecationLogger); $prevHander = set_exception_handler([$handler, 'handleError']); try { @@ -236,4 +241,14 @@ public function testLevelsAssignedToLoggers(bool $hasLogger, bool $hasDeprecatio throw $e; } } + + /** + * @group legacy + */ + public function testLegacyConstructor() + { + $this->expectDeprecation('Since symfony/http-kernel 5.4: Passing a $fileLinkFormat is deprecated.'); + + new DebugHandlersListener(null, null, \E_ALL, \E_ALL, true, 'filelinkformat', true, $this->createMock(LoggerInterface::class)); + } } diff --git a/Tests/EventListener/DisallowRobotsIndexingListenerTest.php b/Tests/EventListener/DisallowRobotsIndexingListenerTest.php index 6534ebf4e2..8ceb910b5b 100644 --- a/Tests/EventListener/DisallowRobotsIndexingListenerTest.php +++ b/Tests/EventListener/DisallowRobotsIndexingListenerTest.php @@ -29,7 +29,7 @@ public function testInvoke(?string $expected, array $responseArgs) $response = new Response(...$responseArgs); $listener = new DisallowRobotsIndexingListener(); - $event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $this->createMock(Request::class), KernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(HttpKernelInterface::class), new Request(), KernelInterface::MAIN_REQUEST, $response); $listener->onResponse($event); diff --git a/Tests/EventListener/ErrorListenerTest.php b/Tests/EventListener/ErrorListenerTest.php index 408b580630..73cc19afc8 100644 --- a/Tests/EventListener/ErrorListenerTest.php +++ b/Tests/EventListener/ErrorListenerTest.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\ErrorListener; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; @@ -97,16 +98,37 @@ public function testHandleWithLogger($event, $event2) $this->assertCount(3, $logger->getLogs('critical')); } + public function testHandleWithLoggerAndCustomConfiguration() + { + $request = new Request(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); + $logger = new TestLogger(); + $l = new ErrorListener('not used', $logger, false, [ + \RuntimeException::class => [ + 'log_level' => 'warning', + 'status_code' => 401, + ], + ]); + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertEquals(new Response('foo', 401), $event->getResponse()); + + $this->assertEquals(0, $logger->countErrors()); + $this->assertCount(0, $logger->getLogs('critical')); + $this->assertCount(1, $logger->getLogs('warning')); + } + public function provider() { - if (!class_exists('Symfony\Component\HttpFoundation\Request')) { + if (!class_exists(Request::class)) { return [[null, null]]; } $request = new Request(); $exception = new \Exception('foo'); - $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MASTER_REQUEST, $exception); - $event2 = new ExceptionEvent(new TestKernelThatThrowsException(), $request, HttpKernelInterface::MASTER_REQUEST, $exception); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $exception); + $event2 = new ExceptionEvent(new TestKernelThatThrowsException(), $request, HttpKernelInterface::MAIN_REQUEST, $exception); return [ [$event, $event2], @@ -115,9 +137,9 @@ public function provider() public function testSubRequestFormat() { - $listener = new ErrorListener('foo', $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock()); + $listener = new ErrorListener('foo', $this->createMock(LoggerInterface::class)); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { return new Response($request->getRequestFormat()); }); @@ -125,7 +147,7 @@ public function testSubRequestFormat() $request = Request::create('/'); $request->setRequestFormat('xml'); - $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new \Exception('foo')); + $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo')); $listener->onKernelException($event); $response = $event->getResponse(); @@ -135,23 +157,23 @@ public function testSubRequestFormat() public function testCSPHeaderIsRemoved() { $dispatcher = new EventDispatcher(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { return new Response($request->getRequestFormat()); }); - $listener = new ErrorListener('foo', $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(), true); + $listener = new ErrorListener('foo', $this->createMock(LoggerInterface::class), true); $dispatcher->addSubscriber($listener); $request = Request::create('/'); - $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new \Exception('foo')); + $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo')); $dispatcher->dispatch($event, KernelEvents::EXCEPTION); $response = new Response('', 200, ['content-security-policy' => "style-src 'self'"]); $this->assertTrue($response->headers->has('content-security-policy')); - $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertFalse($response->headers->has('content-security-policy'), 'CSP header has been removed'); @@ -174,7 +196,7 @@ public function testOnControllerArguments(callable $controller) return $controller(...$event->getArguments()); }); - $event = new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, new \Exception('foo')); + $event = new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, new \Exception('foo')); $listener->onKernelException($event); $this->assertSame('OK: foo', $event->getResponse()->getContent()); @@ -187,7 +209,7 @@ public function controllerProvider() }]; yield [function ($exception) { - $this->assertInstanceOf(FlattenException::class, $exception); + static::assertInstanceOf(FlattenException::class, $exception); return new Response('OK: '.$exception->getMessage()); }]; @@ -208,15 +230,20 @@ public function countErrors(Request $request = null): int class TestKernel implements HttpKernelInterface { - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response + public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { + $e = $request->attributes->get('exception'); + if ($e instanceof HttpExceptionInterface) { + return new Response('foo', $e->getStatusCode(), $e->getHeaders()); + } + return new Response('foo'); } } class TestKernelThatThrowsException implements HttpKernelInterface { - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response + public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { throw new \RuntimeException('bar'); } diff --git a/Tests/EventListener/FragmentListenerTest.php b/Tests/EventListener/FragmentListenerTest.php index 5b045a8fc4..77d07a39f3 100644 --- a/Tests/EventListener/FragmentListenerTest.php +++ b/Tests/EventListener/FragmentListenerTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\FragmentListener; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\UriSigner; @@ -52,7 +53,7 @@ public function testOnlyTriggeredIfControllerWasNotDefinedYet() public function testAccessDeniedWithNonSafeMethods() { - $this->expectException('Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException'); + $this->expectException(AccessDeniedHttpException::class); $request = Request::create('http://example.com/_fragment', 'POST'); $listener = new FragmentListener(new UriSigner('foo')); @@ -63,7 +64,7 @@ public function testAccessDeniedWithNonSafeMethods() public function testAccessDeniedWithWrongSignature() { - $this->expectException('Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException'); + $this->expectException(AccessDeniedHttpException::class); $request = Request::create('http://example.com/_fragment', 'GET', [], [], [], ['REMOTE_ADDR' => '10.0.0.1']); $listener = new FragmentListener(new UriSigner('foo')); @@ -111,8 +112,8 @@ public function testRemovesPathWithControllerNotDefined() $this->assertFalse($request->query->has('_path')); } - private function createRequestEvent(Request $request, $requestType = HttpKernelInterface::MASTER_REQUEST) + private function createRequestEvent(Request $request, int $requestType = HttpKernelInterface::MAIN_REQUEST): RequestEvent { - return new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, $requestType); + return new RequestEvent($this->createMock(HttpKernelInterface::class), $request, $requestType); } } diff --git a/Tests/EventListener/LocaleAwareListenerTest.php b/Tests/EventListener/LocaleAwareListenerTest.php index 0064429048..3c89cafd04 100644 --- a/Tests/EventListener/LocaleAwareListenerTest.php +++ b/Tests/EventListener/LocaleAwareListenerTest.php @@ -28,7 +28,7 @@ class LocaleAwareListenerTest extends TestCase protected function setUp(): void { - $this->localeAwareService = $this->getMockBuilder(LocaleAwareInterface::class)->getMock(); + $this->localeAwareService = $this->createMock(LocaleAwareInterface::class); $this->requestStack = new RequestStack(); $this->listener = new LocaleAwareListener(new \ArrayIterator([$this->localeAwareService]), $this->requestStack); } @@ -40,7 +40,7 @@ public function testLocaleIsSetInOnKernelRequest() ->method('setLocale') ->with($this->equalTo('fr')); - $event = new RequestEvent($this->createHttpKernel(), $this->createRequest('fr'), HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $this->createRequest('fr'), HttpKernelInterface::MAIN_REQUEST); $this->listener->onKernelRequest($event); } @@ -57,7 +57,7 @@ public function testDefaultLocaleIsUsedOnExceptionsInOnKernelRequest() $this->throwException(new \InvalidArgumentException()) ); - $event = new RequestEvent($this->createHttpKernel(), $this->createRequest('fr'), HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $this->createRequest('fr'), HttpKernelInterface::MAIN_REQUEST); $this->listener->onKernelRequest($event); } @@ -71,7 +71,7 @@ public function testLocaleIsSetInOnKernelFinishRequestWhenParentRequestExists() $this->requestStack->push($this->createRequest('fr')); $this->requestStack->push($subRequest = $this->createRequest('de')); - $event = new FinishRequestEvent($this->createHttpKernel(), $subRequest, HttpKernelInterface::SUB_REQUEST); + $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $this->listener->onKernelFinishRequest($event); } @@ -84,7 +84,7 @@ public function testLocaleIsSetToDefaultOnKernelFinishRequestWhenParentRequestDo $this->requestStack->push($subRequest = $this->createRequest('de')); - $event = new FinishRequestEvent($this->createHttpKernel(), $subRequest, HttpKernelInterface::SUB_REQUEST); + $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $this->listener->onKernelFinishRequest($event); } @@ -104,15 +104,10 @@ public function testDefaultLocaleIsUsedOnExceptionsInOnKernelFinishRequest() $this->requestStack->push($this->createRequest('fr')); $this->requestStack->push($subRequest = $this->createRequest('de')); - $event = new FinishRequestEvent($this->createHttpKernel(), $subRequest, HttpKernelInterface::SUB_REQUEST); + $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $this->listener->onKernelFinishRequest($event); } - private function createHttpKernel() - { - return $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); - } - private function createRequest($locale) { $request = new Request(); diff --git a/Tests/EventListener/LocaleListenerTest.php b/Tests/EventListener/LocaleListenerTest.php index cb502a89ee..824d906340 100644 --- a/Tests/EventListener/LocaleListenerTest.php +++ b/Tests/EventListener/LocaleListenerTest.php @@ -14,11 +14,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\LocaleListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Router; class LocaleListenerTest extends TestCase { @@ -26,7 +29,7 @@ class LocaleListenerTest extends TestCase protected function setUp(): void { - $this->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->disableOriginalConstructor()->getMock(); + $this->requestStack = $this->createMock(RequestStack::class); } public function testIsAnEventSubscriber() @@ -70,10 +73,10 @@ public function testLocaleFromRequestAttribute() public function testLocaleSetForRoutingContext() { // the request context is updated - $context = $this->getMockBuilder('Symfony\Component\Routing\RequestContext')->getMock(); + $context = $this->createMock(RequestContext::class); $context->expects($this->once())->method('setParameter')->with('_locale', 'es'); - $router = $this->getMockBuilder('Symfony\Component\Routing\Router')->setMethods(['getContext'])->disableOriginalConstructor()->getMock(); + $router = $this->getMockBuilder(Router::class)->setMethods(['getContext'])->disableOriginalConstructor()->getMock(); $router->expects($this->once())->method('getContext')->willReturn($context); $request = Request::create('/'); @@ -86,10 +89,10 @@ public function testLocaleSetForRoutingContext() public function testRouterResetWithParentRequestOnKernelFinishRequest() { // the request context is updated - $context = $this->getMockBuilder('Symfony\Component\Routing\RequestContext')->getMock(); + $context = $this->createMock(RequestContext::class); $context->expects($this->once())->method('setParameter')->with('_locale', 'es'); - $router = $this->getMockBuilder('Symfony\Component\Routing\Router')->setMethods(['getContext'])->disableOriginalConstructor()->getMock(); + $router = $this->getMockBuilder(Router::class)->setMethods(['getContext'])->disableOriginalConstructor()->getMock(); $router->expects($this->once())->method('getContext')->willReturn($context); $parentRequest = Request::create('/'); @@ -97,7 +100,7 @@ public function testRouterResetWithParentRequestOnKernelFinishRequest() $this->requestStack->expects($this->once())->method('getParentRequest')->willReturn($parentRequest); - $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST); + $event = new FinishRequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST); $listener = new LocaleListener($this->requestStack, 'fr', $router); $listener->onKernelFinishRequest($event); @@ -114,8 +117,87 @@ public function testRequestLocaleIsNotOverridden() $this->assertEquals('de', $request->getLocale()); } + public function testRequestPreferredLocaleFromAcceptLanguageHeader() + { + $request = Request::create('/'); + $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); + + $listener = new LocaleListener($this->requestStack, 'de', null, true, ['de', 'fr']); + $event = $this->getEvent($request); + + $listener->setDefaultLocale($event); + $listener->onKernelRequest($event); + $this->assertEquals('fr', $request->getLocale()); + } + + public function testRequestSecondPreferredLocaleFromAcceptLanguageHeader() + { + $request = Request::create('/'); + $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); + + $listener = new LocaleListener($this->requestStack, 'de', null, true, ['de', 'en']); + $event = $this->getEvent($request); + + $listener->setDefaultLocale($event); + $listener->onKernelRequest($event); + $this->assertEquals('en', $request->getLocale()); + } + + public function testDontUseAcceptLanguageHeaderIfNotEnabled() + { + $request = Request::create('/'); + $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); + + $listener = new LocaleListener($this->requestStack, 'de', null, false, ['de', 'en']); + $event = $this->getEvent($request); + + $listener->setDefaultLocale($event); + $listener->onKernelRequest($event); + $this->assertEquals('de', $request->getLocale()); + } + + public function testRequestUnavailablePreferredLocaleFromAcceptLanguageHeader() + { + $request = Request::create('/'); + $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); + + $listener = new LocaleListener($this->requestStack, 'de', null, true, ['de', 'it']); + $event = $this->getEvent($request); + + $listener->setDefaultLocale($event); + $listener->onKernelRequest($event); + $this->assertEquals('de', $request->getLocale()); + } + + public function testRequestNoLocaleFromAcceptLanguageHeader() + { + $request = Request::create('/'); + $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); + + $listener = new LocaleListener($this->requestStack, 'de', null, true); + $event = $this->getEvent($request); + + $listener->setDefaultLocale($event); + $listener->onKernelRequest($event); + $this->assertEquals('de', $request->getLocale()); + } + + public function testRequestAttributeLocaleNotOverridenFromAcceptLanguageHeader() + { + $request = Request::create('/'); + $request->attributes->set('_locale', 'it'); + $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); + + $listener = new LocaleListener($this->requestStack, 'de', null, true, ['fr', 'en']); + $event = $this->getEvent($request); + + $listener->setDefaultLocale($event); + $listener->onKernelRequest($event); + $this->assertEquals('it', $request->getLocale()); + } + private function getEvent(Request $request): RequestEvent { - return new RequestEvent($this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); + return new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); } } diff --git a/Tests/EventListener/ProfilerListenerTest.php b/Tests/EventListener/ProfilerListenerTest.php index 3aaff12316..45272ceb02 100644 --- a/Tests/EventListener/ProfilerListenerTest.php +++ b/Tests/EventListener/ProfilerListenerTest.php @@ -12,59 +12,89 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\HttpKernel\Profiler\Profiler; class ProfilerListenerTest extends TestCase { /** - * Test a master and sub request with an exception and `onlyException` profiler option enabled. + * Test a main and sub request with an exception and `onlyException` profiler option enabled. */ public function testKernelTerminate() { $profile = new Profile('token'); - $profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); - + $profiler = $this->createMock(Profiler::class); $profiler->expects($this->once()) ->method('collect') ->willReturn($profile); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); - - $masterRequest = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') - ->disableOriginalConstructor() - ->getMock(); - - $subRequest = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') - ->disableOriginalConstructor() - ->getMock(); - - $response = $this->getMockBuilder('Symfony\Component\HttpFoundation\Response') - ->disableOriginalConstructor() - ->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); + $mainRequest = $this->createMock(Request::class); + $subRequest = $this->createMock(Request::class); + $response = $this->createMock(Response::class); $requestStack = new RequestStack(); - $requestStack->push($masterRequest); + $requestStack->push($mainRequest); $onlyException = true; $listener = new ProfilerListener($profiler, $requestStack, null, $onlyException); - // master request - $listener->onKernelResponse(new ResponseEvent($kernel, $masterRequest, Kernel::MASTER_REQUEST, $response)); + // main request + $listener->onKernelResponse(new ResponseEvent($kernel, $mainRequest, Kernel::MAIN_REQUEST, $response)); // sub request $listener->onKernelException(new ExceptionEvent($kernel, $subRequest, Kernel::SUB_REQUEST, new HttpException(404))); $listener->onKernelResponse(new ResponseEvent($kernel, $subRequest, Kernel::SUB_REQUEST, $response)); - $listener->onKernelTerminate(new TerminateEvent($kernel, $masterRequest, $response)); + $listener->onKernelTerminate(new TerminateEvent($kernel, $mainRequest, $response)); + } + + /** + * @dataProvider collectRequestProvider + */ + public function testCollectParameter(Request $request, ?bool $enable) + { + $profile = new Profile('token'); + + $profiler = $this->createMock(Profiler::class); + $profiler->expects($this->once()) + ->method('collect') + ->willReturn($profile); + + $profiler + ->expects(null === $enable ? $this->never() : $this->once()) + ->method($enable ? 'enable' : 'disable'); + + $kernel = $this->createMock(HttpKernelInterface::class); + $response = new Response(); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $listener = new ProfilerListener($profiler, $requestStack, null, false, false, 'profile'); + + $listener->onKernelResponse(new ResponseEvent($kernel, $request, Kernel::MAIN_REQUEST, $response)); + } + + public function collectRequestProvider(): iterable + { + yield [Request::create('/'), null]; + yield [Request::create('/', 'GET', ['profile' => '1']), true]; + yield [Request::create('/', 'GET', ['profile' => '0']), false]; + + $request = Request::create('/'); + $request->attributes->set('profile', true); + yield [$request, true]; } } diff --git a/Tests/EventListener/ResponseListenerTest.php b/Tests/EventListener/ResponseListenerTest.php index 1aaa64bc89..f6ca1963c8 100644 --- a/Tests/EventListener/ResponseListenerTest.php +++ b/Tests/EventListener/ResponseListenerTest.php @@ -32,7 +32,7 @@ protected function setUp(): void $listener = new ResponseListener('UTF-8'); $this->dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'onKernelResponse']); - $this->kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $this->kernel = $this->createMock(HttpKernelInterface::class); } protected function tearDown(): void @@ -58,7 +58,7 @@ public function testFilterSetsNonDefaultCharsetIfNotOverridden() $response = new Response('foo'); - $event = new ResponseEvent($this->kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('ISO-8859-15', $response->getCharset()); @@ -72,7 +72,7 @@ public function testFilterDoesNothingIfCharsetIsOverridden() $response = new Response('foo'); $response->setCharset('ISO-8859-1'); - $event = new ResponseEvent($this->kernel, Request::create('/'), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('ISO-8859-1', $response->getCharset()); @@ -87,9 +87,55 @@ public function testFiltersSetsNonDefaultCharsetIfNotOverriddenOnNonTextContentT $request = Request::create('/'); $request->setRequestFormat('application/json'); - $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('ISO-8859-15', $response->getCharset()); } + + public function testSetContentLanguageHeaderWhenEmptyAndAtLeast2EnabledLocalesAreConfigured() + { + $listener = new ResponseListener('ISO-8859-15', true, ['fr', 'en']); + $this->dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'onKernelResponse'], 1); + + $response = new Response('content'); + $request = Request::create('/'); + $request->setLocale('fr'); + + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); + $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); + + $this->assertEquals('fr', $response->headers->get('Content-Language')); + } + + public function testNotOverrideContentLanguageHeaderWhenNotEmpty() + { + $listener = new ResponseListener('ISO-8859-15', true, ['de']); + $this->dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'onKernelResponse'], 1); + + $response = new Response('content'); + $response->headers->set('Content-Language', 'mi, en'); + $request = Request::create('/'); + $request->setLocale('de'); + + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); + $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); + + $this->assertEquals('mi, en', $response->headers->get('Content-Language')); + } + + public function testNotSetContentLanguageHeaderWhenDisabled() + { + $listener = new ResponseListener('ISO-8859-15', false); + $this->dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'onKernelResponse'], 1); + + $response = new Response('content'); + $request = Request::create('/'); + $request->setLocale('fr'); + + $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); + $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); + + $this->assertNull($response->headers->get('Content-Language')); + } } diff --git a/Tests/EventListener/RouterListenerTest.php b/Tests/EventListener/RouterListenerTest.php index 2c1e7721b4..e61414bd04 100644 --- a/Tests/EventListener/RouterListenerTest.php +++ b/Tests/EventListener/RouterListenerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -22,9 +23,16 @@ use Symfony\Component\HttpKernel\EventListener\ErrorListener; use Symfony\Component\HttpKernel\EventListener\RouterListener; use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\RequestContext; class RouterListenerTest extends TestCase @@ -33,7 +41,7 @@ class RouterListenerTest extends TestCase protected function setUp(): void { - $this->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->disableOriginalConstructor()->getMock(); + $this->requestStack = $this->createMock(RequestStack::class); } /** @@ -41,9 +49,8 @@ protected function setUp(): void */ public function testPort($defaultHttpPort, $defaultHttpsPort, $uri, $expectedHttpPort, $expectedHttpsPort) { - $urlMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\UrlMatcherInterface') - ->disableOriginalConstructor() - ->getMock(); + $urlMatcher = $this->createMock(UrlMatcherInterface::class); + $context = new RequestContext(); $context->setHttpPort($defaultHttpPort); $context->setHttpsPort($defaultHttpsPort); @@ -57,7 +64,7 @@ public function testPort($defaultHttpPort, $defaultHttpsPort, $uri, $expectedHtt $this->assertEquals($expectedHttpPort, $context->getHttpPort()); $this->assertEquals($expectedHttpsPort, $context->getHttpsPort()); - $this->assertEquals(0 === strpos($uri, 'https') ? 'https' : 'http', $context->getScheme()); + $this->assertEquals(str_starts_with($uri, 'https') ? 'https' : 'http', $context->getScheme()); } public function getPortData() @@ -72,29 +79,29 @@ public function getPortData() private function createRequestEventForUri(string $uri): RequestEvent { - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create($uri); $request->attributes->set('_controller', null); // Prevents going in to routing process - return new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); } public function testInvalidMatcher() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new RouterListener(new \stdClass(), $this->requestStack); } public function testRequestMatcher() { - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('http://localhost/'); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - $requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock(); + $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->once()) ->method('matchRequest') - ->with($this->isInstanceOf('Symfony\Component\HttpFoundation\Request')) + ->with($this->isInstanceOf(Request::class)) ->willReturn([]); $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext()); @@ -103,14 +110,14 @@ public function testRequestMatcher() public function testSubRequestWithDifferentMethod() { - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('http://localhost/', 'post'); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - $requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock(); + $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->any()) ->method('matchRequest') - ->with($this->isInstanceOf('Symfony\Component\HttpFoundation\Request')) + ->with($this->isInstanceOf(Request::class)) ->willReturn([]); $context = new RequestContext(); @@ -119,7 +126,7 @@ public function testSubRequestWithDifferentMethod() $listener->onKernelRequest($event); // sub-request with another HTTP method - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('http://localhost/', 'get'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST); @@ -133,21 +140,21 @@ public function testSubRequestWithDifferentMethod() */ public function testLoggingParameter($parameter, $log, $parameters) { - $requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock(); + $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->once()) ->method('matchRequest') ->willReturn($parameter); - $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('info') ->with($this->equalTo($log), $this->equalTo($parameters)); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('http://localhost/'); $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext(), $logger); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); } public function getLoggingParameterData() @@ -162,7 +169,7 @@ public function testWithBadRequest() { $requestStack = new RequestStack(); - $requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock(); + $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->never())->method('matchRequest'); $dispatcher = new EventDispatcher(); @@ -184,7 +191,7 @@ public function testNoRoutingConfigurationResponse() { $requestStack = new RequestStack(); - $requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock(); + $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher ->expects($this->once()) ->method('matchRequest') @@ -204,14 +211,67 @@ public function testNoRoutingConfigurationResponse() public function testRequestWithBadHost() { - $this->expectException('Symfony\Component\HttpKernel\Exception\BadRequestHttpException'); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $this->expectException(BadRequestHttpException::class); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('http://bad host %22/'); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); - $requestMatcher = $this->getMockBuilder('Symfony\Component\Routing\Matcher\RequestMatcherInterface')->getMock(); + $requestMatcher = $this->createMock(RequestMatcherInterface::class); $listener = new RouterListener($requestMatcher, $this->requestStack, new RequestContext()); $listener->onKernelRequest($event); } + + public function testResourceNotFoundException() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('No route found for "GET https://www.symfony.com/path" (from "https://www.google.com/")'); + + $context = new RequestContext(); + + $urlMatcher = $this->createMock(UrlMatcherInterface::class); + + $urlMatcher->expects($this->any()) + ->method('getContext') + ->willReturn($context); + + $urlMatcher->expects($this->any()) + ->method('match') + ->willThrowException(new ResourceNotFoundException()); + + $kernel = $this->createMock(HttpKernelInterface::class); + $request = Request::create('https://www.symfony.com/path'); + $request->headers->set('referer', 'https://www.google.com/'); + + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener = new RouterListener($urlMatcher, $this->requestStack); + $listener->onKernelRequest($event); + } + + public function testMethodNotAllowedException() + { + $this->expectException(MethodNotAllowedHttpException::class); + $this->expectExceptionMessage('No route found for "GET https://www.symfony.com/path": Method Not Allowed (Allow: POST)'); + + $context = new RequestContext(); + + $urlMatcher = $this->createMock(UrlMatcherInterface::class); + + $urlMatcher->expects($this->any()) + ->method('getContext') + ->willReturn($context); + + $urlMatcher->expects($this->any()) + ->method('match') + ->willThrowException(new MethodNotAllowedException(['POST'])); + + $kernel = $this->createMock(HttpKernelInterface::class); + $request = Request::create('https://www.symfony.com/path'); + + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $listener = new RouterListener($urlMatcher, $this->requestStack); + $listener->onKernelRequest($event); + } } diff --git a/Tests/EventListener/SessionListenerTest.php b/Tests/EventListener/SessionListenerTest.php index 8df2ce5169..96f66354ba 100644 --- a/Tests/EventListener/SessionListenerTest.php +++ b/Tests/EventListener/SessionListenerTest.php @@ -15,11 +15,17 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionFactory; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorageFactory; +use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorageFactory; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; +use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; @@ -27,14 +33,314 @@ use Symfony\Component\HttpKernel\EventListener\SessionListener; use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelInterface; class SessionListenerTest extends TestCase { - public function testOnlyTriggeredOnMasterRequest() + /** + * @dataProvider provideSessionOptions + * + * @runInSeparateProcess + */ + public function testSessionCookieOptions(array $phpSessionOptions, array $sessionOptions, array $expectedSessionOptions) + { + $session = $this->createMock(Session::class); + $session->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + $session->method('getId')->willReturn('123456'); + $session->method('getName')->willReturn('PHPSESSID'); + $session->method('save'); + $session->method('isStarted')->willReturn(true); + + if (isset($phpSessionOptions['samesite'])) { + ini_set('session.cookie_samesite', $phpSessionOptions['samesite']); + } + session_set_cookie_params(0, $phpSessionOptions['path'] ?? null, $phpSessionOptions['domain'] ?? null, $phpSessionOptions['secure'] ?? null, $phpSessionOptions['httponly'] ?? null); + + $listener = new SessionListener(new Container(), false, $sessionOptions); + $kernel = $this->createMock(HttpKernelInterface::class); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $request->setSession($session); + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + + if ($sessionOptions['use_cookies'] ?? true) { + $this->assertCount(1, $cookies); + $this->assertSame('PHPSESSID', $cookies[0]->getName()); + $this->assertSame('123456', $cookies[0]->getValue()); + $this->assertSame($expectedSessionOptions['cookie_path'], $cookies[0]->getPath()); + $this->assertSame($expectedSessionOptions['cookie_domain'], $cookies[0]->getDomain()); + $this->assertSame($expectedSessionOptions['cookie_secure'], $cookies[0]->isSecure()); + $this->assertSame($expectedSessionOptions['cookie_httponly'], $cookies[0]->isHttpOnly()); + $this->assertSame($expectedSessionOptions['cookie_samesite'], $cookies[0]->getSameSite()); + } else { + $this->assertCount(0, $cookies); + } + } + + public function provideSessionOptions(): \Generator + { + if (\PHP_VERSION_ID > 70300) { + yield 'set_samesite_by_php' => [ + 'phpSessionOptions' => ['samesite' => Cookie::SAMESITE_STRICT], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_STRICT], + ]; + } + + yield 'set_cookie_path_by_php' => [ + 'phpSessionOptions' => ['path' => '/prod/'], + 'sessionOptions' => ['cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/prod/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookie_secure_by_php' => [ + 'phpSessionOptions' => ['secure' => true], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookiesecure_auto_by_symfony_false_by_php' => [ + 'phpSessionOptions' => ['secure' => false], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => 'auto', 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => false, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookiesecure_auto_by_symfony_true_by_php' => [ + 'phpSessionOptions' => ['secure' => true], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => 'auto', 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookie_httponly_by_php' => [ + 'phpSessionOptions' => ['httponly' => true], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_cookie_domain_by_php' => [ + 'phpSessionOptions' => ['domain' => 'test.symfony'], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => 'test.symfony', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_samesite_by_symfony' => [ + 'phpSessionOptions' => ['samesite' => Cookie::SAMESITE_STRICT], + 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + ]; + + yield 'set_use_cookies_false_by_symfony' => [ + 'phpSessionOptions' => [], + 'sessionOptions' => ['use_cookies' => false, 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], + 'expectedSessionOptions' => [], + ]; + } + + /** + * @runInSeparateProcess + */ + public function testPhpBridgeAlreadyStartedSession() + { + session_start(); + $sessionId = session_id(); + + $request = new Request(); + $listener = $this->createListener($request, new PhpBridgeSessionStorageFactory()); + + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + + $listener->onKernelRequest($event); + + $this->assertTrue($request->hasSession()); + $this->assertSame($sessionId, $request->getSession()->getId()); + } + + /** + * @runInSeparateProcess + */ + public function testSessionCookieWrittenNoCookieGiven() + { + $request = new Request(); + $listener = $this->createListener($request, new NativeSessionStorageFactory()); + + $kernel = $this->createMock(HttpKernelInterface::class); + + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + $session = $request->getSession(); + $session->set('hello', 'world'); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertCount(1, $cookies); + $sessionCookie = $cookies[0]; + + $this->assertSame('PHPSESSID', $sessionCookie->getName()); + $this->assertNotEmpty($sessionCookie->getValue()); + $this->assertFalse($sessionCookie->isCleared()); + } + + /** + * @runInSeparateProcess + */ + public function testSessionCookieNotWrittenCookieGiven() + { + $sessionId = $this->createValidSessionId(); + + $this->assertNotEmpty($sessionId); + + $request = new Request(); + $request->cookies->set('PHPSESSID', $sessionId); + + $listener = $this->createListener($request, new NativeSessionStorageFactory()); + + $kernel = $this->createMock(HttpKernelInterface::class); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $session = $request->getSession(); + $this->assertSame($sessionId, $session->getId()); + $session->set('hello', 'world'); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + $this->assertSame($sessionId, $session->getId()); + + $cookies = $response->headers->getCookies(); + $this->assertCount(0, $cookies); + } + + /** + * @runInSeparateProcess + */ + public function testNewSessionIdIsNotOverwritten() + { + $newSessionId = $this->createValidSessionId(); + + $this->assertNotEmpty($newSessionId); + + $request = new Request(); + $request->cookies->set('PHPSESSID', 'OLD-SESSION-ID'); + + $listener = $this->createListener($request, new NativeSessionStorageFactory()); + + $kernel = $this->createMock(HttpKernelInterface::class); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $session = $request->getSession(); + $this->assertSame($newSessionId, $session->getId()); + $session->set('hello', 'world'); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + $this->assertSame($newSessionId, $session->getId()); + + $cookies = $response->headers->getCookies(); + + $this->assertCount(1, $cookies); + $sessionCookie = $cookies[0]; + + $this->assertSame('PHPSESSID', $sessionCookie->getName()); + $this->assertSame($newSessionId, $sessionCookie->getValue()); + } + + /** + * @runInSeparateProcess + */ + public function testSessionCookieClearedWhenInvalidated() + { + $sessionId = $this->createValidSessionId(); + $request = new Request(); + $request->cookies->set('PHPSESSID', $sessionId); + $listener = $this->createListener($request, new NativeSessionStorageFactory()); + $kernel = $this->createMock(HttpKernelInterface::class); + + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $session = $request->getSession(); + $session->start(); + $sessionId = $session->getId(); + $this->assertNotEmpty($sessionId); + $_SESSION['hello'] = 'world'; // check compatibility to php session bridge + + $session->invalidate(); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertCount(1, $cookies); + $sessionCookie = $cookies[0]; + + $this->assertSame('PHPSESSID', $sessionCookie->getName()); + $this->assertTrue($sessionCookie->isCleared()); + } + + /** + * @runInSeparateProcess + */ + public function testSessionCookieNotClearedWhenOtherVariablesSet() + { + $sessionId = $this->createValidSessionId(); + $request = new Request(); + $request->cookies->set('PHPSESSID', $sessionId); + $listener = $this->createListener($request, new NativeSessionStorageFactory()); + $kernel = $this->createMock(HttpKernelInterface::class); + + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $session = $request->getSession(); + $session->start(); + $sessionId = $session->getId(); + $this->assertNotEmpty($sessionId); + $_SESSION['hello'] = 'world'; + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertCount(0, $cookies); + } + + /** + * @runInSeparateProcess + */ + public function testSessionCookieSetWhenOtherNativeVariablesSet() + { + $request = new Request(); + $listener = $this->createListener($request, new NativeSessionStorageFactory()); + $kernel = $this->createMock(HttpKernelInterface::class); + + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $session = $request->getSession(); + $session->start(); + $sessionId = $session->getId(); + $this->assertNotEmpty($sessionId); + $_SESSION['hello'] = 'world'; + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertCount(1, $cookies); + $sessionCookie = $cookies[0]; + + $this->assertSame('PHPSESSID', $sessionCookie->getName()); + $this->assertNotEmpty($sessionCookie->getValue()); + $this->assertFalse($sessionCookie->isCleared()); + } + + public function testOnlyTriggeredOnMainRequest() { $listener = $this->getMockForAbstractClass(AbstractSessionListener::class); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event->expects($this->once())->method('isMasterRequest')->willReturn(false); + $event = $this->createMock(RequestEvent::class); + $event->expects($this->once())->method('isMainRequest')->willReturn(false); $event->expects($this->never())->method('getRequest'); // sub request @@ -43,12 +349,13 @@ public function testOnlyTriggeredOnMasterRequest() public function testSessionIsSet() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); - $requestStack = $this->getMockBuilder(RequestStack::class)->getMock(); - $requestStack->expects($this->once())->method('getMasterRequest')->willReturn(null); + $requestStack = $this->createMock(RequestStack::class); + $requestStack->expects($this->once())->method('getMainRequest')->willReturn(null); - $sessionStorage = $this->getMockBuilder(NativeSessionStorage::class)->getMock(); + $sessionStorage = $this->createMock(NativeSessionStorage::class); $sessionStorage->expects($this->never())->method('setOptions')->with(['cookie_secure' => true]); $container = new Container(); @@ -59,9 +366,28 @@ public function testSessionIsSet() $request = new Request(); $listener = new SessionListener($container); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event->expects($this->once())->method('isMasterRequest')->willReturn(true); - $event->expects($this->once())->method('getRequest')->willReturn($request); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + + $listener->onKernelRequest($event); + + $this->assertTrue($request->hasSession()); + $this->assertSame($session, $request->getSession()); + } + + public function testSessionUsesFactory() + { + $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); + $sessionFactory = $this->createMock(SessionFactory::class); + $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); + + $container = new Container(); + $container->set('session_factory', $sessionFactory); + + $request = new Request(); + $listener = new SessionListener($container); + + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); @@ -71,47 +397,47 @@ public function testSessionIsSet() public function testResponseIsPrivateIfSessionStarted() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); $container = new Container(); $container->set('initialized_session', $session); $listener = new SessionListener($container); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); - $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->has('Expires')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); - $this->assertLessThanOrEqual((new \DateTime('now', new \DateTimeZone('UTC'))), (new \DateTime($response->headers->get('Expires')))); + $this->assertLessThanOrEqual(new \DateTime('now', new \DateTimeZone('UTC')), new \DateTime($response->headers->get('Expires'))); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testResponseIsStillPublicIfSessionStartedAndHeaderPresent() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); $container = new Container(); $container->set('initialized_session', $session); $listener = new SessionListener($container); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); $response->setSharedMaxAge(60); $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); - $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->has('Expires')); @@ -121,9 +447,35 @@ public function testResponseIsStillPublicIfSessionStartedAndHeaderPresent() $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } - public function testUninitializedSession() + public function testSessionSaveAndResponseHasSessionCookie() { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); + $session->expects($this->exactly(1))->method('save'); + $session->expects($this->exactly(1))->method('isStarted')->willReturn(true); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container); $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertSame('PHPSESSID', $cookies[0]->getName()); + $this->assertSame('123456', $cookies[0]->getValue()); + } + + public function testUninitializedSessionUsingInitializedSessionService() + { + $kernel = $this->createMock(HttpKernelInterface::class); $response = new Response(); $response->setSharedMaxAge(60); $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); @@ -133,7 +485,7 @@ public function testUninitializedSession() ]); $listener = new SessionListener($container); - $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertFalse($response->headers->has('Expires')); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); @@ -142,9 +494,106 @@ public function testUninitializedSession() $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } - public function testSurrogateMasterRequestIsPublic() + public function testUninitializedSessionUsingSessionFromRequest() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); + $response = new Response(); + $response->setSharedMaxAge(60); + $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); + + $request = new Request(); + $request->setSession(new Session()); + + $listener = new SessionListener(new Container()); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + $this->assertFalse($response->headers->has('Expires')); + $this->assertTrue($response->headers->hasCacheControlDirective('public')); + $this->assertFalse($response->headers->hasCacheControlDirective('private')); + $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); + $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); + } + + public function testUninitializedSessionWithoutInitializedSession() + { + $kernel = $this->createMock(HttpKernelInterface::class); + $response = new Response(); + $response->setSharedMaxAge(60); + $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); + + $container = new ServiceLocator([]); + + $listener = new SessionListener($container); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); + $this->assertFalse($response->headers->has('Expires')); + $this->assertTrue($response->headers->hasCacheControlDirective('public')); + $this->assertFalse($response->headers->hasCacheControlDirective('private')); + $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); + } + + public function testResponseHeadersMaxAgeAndExpiresNotBeOverridenIfSessionStarted() + { + $session = $this->createMock(Session::class); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container); + $kernel = $this->createMock(HttpKernelInterface::class); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $response = new Response(); + $response->setPrivate(); + $expiresHeader = gmdate('D, d M Y H:i:s', time() + 600).' GMT'; + $response->setMaxAge(600); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); + + $this->assertTrue($response->headers->has('expires')); + $this->assertSame($expiresHeader, $response->headers->get('expires')); + $this->assertFalse($response->headers->has('max-age')); + $this->assertSame('600', $response->headers->getCacheControlDirective('max-age')); + $this->assertFalse($response->headers->hasCacheControlDirective('public')); + $this->assertTrue($response->headers->hasCacheControlDirective('private')); + $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); + } + + public function testResponseHeadersMaxAgeAndExpiresDefaultValuesIfSessionStarted() + { + $session = $this->createMock(Session::class); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container); + $kernel = $this->createMock(HttpKernelInterface::class); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); + + $response = new Response(); + $expiresHeader = gmdate('D, d M Y H:i:s', time()).' GMT'; + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); + + $this->assertTrue($response->headers->has('expires')); + $this->assertSame($expiresHeader, $response->headers->get('expires')); + $this->assertFalse($response->headers->has('max-age')); + $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); + $this->assertFalse($response->headers->hasCacheControlDirective('public')); + $this->assertTrue($response->headers->hasCacheControlDirective('private')); + $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); + } + + public function testSurrogateMainRequestIsPublic() + { + $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->expects($this->exactly(4))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1, 1, 1)); $container = new Container(); @@ -152,60 +601,97 @@ public function testSurrogateMasterRequestIsPublic() $container->set('session', $session); $listener = new SessionListener($container); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $response = new Response(); $response->setCache(['public' => true, 'max_age' => '30']); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $this->assertTrue($request->hasSession()); $subRequest = clone $request; $this->assertSame($request->getSession(), $subRequest->getSession()); - $listener->onKernelRequest(new RequestEvent($kernel, $subRequest, HttpKernelInterface::MASTER_REQUEST)); - $listener->onKernelResponse(new ResponseEvent($kernel, $subRequest, HttpKernelInterface::MASTER_REQUEST, $response)); - $listener->onFinishRequest(new FinishRequestEvent($kernel, $subRequest, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $subRequest, HttpKernelInterface::MAIN_REQUEST)); + $listener->onKernelResponse(new ResponseEvent($kernel, $subRequest, HttpKernelInterface::MAIN_REQUEST, $response)); + $listener->onFinishRequest(new FinishRequestEvent($kernel, $subRequest, HttpKernelInterface::MAIN_REQUEST)); $this->assertFalse($response->headers->has('Expires')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('30', $response->headers->getCacheControlDirective('max-age')); - $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); $this->assertTrue($response->headers->has('Expires')); - $this->assertLessThanOrEqual((new \DateTime('now', new \DateTimeZone('UTC'))), (new \DateTime($response->headers->get('Expires')))); + $this->assertLessThanOrEqual(new \DateTime('now', new \DateTimeZone('UTC')), new \DateTime($response->headers->get('Expires'))); + } + + public function testGetSessionIsCalledOnce() + { + $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); + $sessionStorage = $this->createMock(NativeSessionStorage::class); + $kernel = $this->createMock(KernelInterface::class); + + $sessionStorage->expects($this->once()) + ->method('setOptions') + ->with(['cookie_secure' => true]); + + $requestStack = new RequestStack(); + $requestStack->push($mainRequest = new Request([], [], [], [], [], ['HTTPS' => 'on'])); + + $container = new Container(); + $container->set('session_storage', $sessionStorage); + $container->set('session', $session); + $container->set('request_stack', $requestStack); + + $event = new RequestEvent($kernel, $mainRequest, HttpKernelInterface::MAIN_REQUEST); + + $listener = new SessionListener($container); + $listener->onKernelRequest($event); + + // storage->setOptions() should have been called already + $container->set('session_storage', null); + $sessionStorage = null; + + $subRequest = $mainRequest->duplicate(); + // at this point both main and subrequest have a closure to build the session + + $mainRequest->getSession(); + + // calling the factory on the subRequest should not trigger a second call to storage->setOptions() + $subRequest->getSession(); } public function testSessionUsageExceptionIfStatelessAndSessionUsed() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); $container = new Container(); $container->set('initialized_session', $session); $listener = new SessionListener($container, true); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_stateless', true); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $this->expectException(UnexpectedSessionUsageException::class); - $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response())); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response())); } public function testSessionUsageLogIfStatelessAndSessionUsed() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); - $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); + $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->exactly(1))->method('warning'); $container = new Container(); @@ -213,18 +699,20 @@ public function testSessionUsageLogIfStatelessAndSessionUsed() $container->set('logger', $logger); $listener = new SessionListener($container, false); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_stateless', true); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); - $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, new Response())); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response())); } public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); $session->expects($this->exactly(1))->method('save'); @@ -233,21 +721,21 @@ public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown() $container->set('initialized_session', $session); $listener = new SessionListener($container, true); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_stateless', true); - $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); $this->expectException(UnexpectedSessionUsageException::class); - $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST, $response)); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); } public function testSessionUsageCallbackWhenDebugAndStateless() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(1))->method('save'); @@ -260,9 +748,13 @@ public function testSessionUsageCallbackWhenDebugAndStateless() $requestStack->push($request); $requestStack->push(new Request()); + $collector = $this->createMock(RequestDataCollector::class); + $collector->expects($this->once())->method('collectSessionUsage'); + $container = new Container(); $container->set('initialized_session', $session); $container->set('request_stack', $requestStack); + $container->set('session_collector', \Closure::fromCallable([$collector, 'collectSessionUsage'])); $this->expectException(UnexpectedSessionUsageException::class); (new SessionListener($container, true))->onSessionUsage(); @@ -270,26 +762,30 @@ public function testSessionUsageCallbackWhenDebugAndStateless() public function testSessionUsageCallbackWhenNoDebug() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(0))->method('save'); $request = new Request(); $request->attributes->set('_stateless', true); - $requestStack = $this->getMockBuilder(RequestStack::class)->getMock(); - $requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $collector = $this->createMock(RequestDataCollector::class); + $collector->expects($this->never())->method('collectSessionUsage'); $container = new Container(); $container->set('initialized_session', $session); $container->set('request_stack', $requestStack); + $container->set('session_collector', $collector); (new SessionListener($container))->onSessionUsage(); } public function testSessionUsageCallbackWhenNoStateless() { - $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn(true); $session->expects($this->never())->method('save'); @@ -303,4 +799,75 @@ public function testSessionUsageCallbackWhenNoStateless() (new SessionListener($container, true))->onSessionUsage(); } + + /** + * @runInSeparateProcess + */ + public function testReset() + { + session_start(); + $_SESSION['test'] = ['test']; + session_write_close(); + + $this->assertNotEmpty($_SESSION); + $this->assertNotEmpty(session_id()); + + $container = new Container(); + + (new SessionListener($container, true))->reset(); + + $this->assertEmpty($_SESSION); + $this->assertEmpty(session_id()); + $this->assertSame(\PHP_SESSION_NONE, session_status()); + } + + /** + * @runInSeparateProcess + */ + public function testResetUnclosedSession() + { + session_start(); + $_SESSION['test'] = ['test']; + + $this->assertNotEmpty($_SESSION); + $this->assertNotEmpty(session_id()); + $this->assertSame(\PHP_SESSION_ACTIVE, session_status()); + + $container = new Container(); + + (new SessionListener($container, true))->reset(); + + $this->assertEmpty($_SESSION); + $this->assertEmpty(session_id()); + $this->assertSame(\PHP_SESSION_NONE, session_status()); + } + + private function createListener(Request $request, SessionStorageFactoryInterface $sessionFactory) + { + $requestStack = new RequestStack(); + $request = new Request(); + $requestStack->push($request); + + $sessionFactory = new SessionFactory($requestStack, $sessionFactory); + + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('session_factory', $sessionFactory); + + $listener = new SessionListener($container); + + return new SessionListener($container); + } + + private function createValidSessionId(): string + { + session_start(); + $sessionId = session_id(); + $_SESSION['some'] = 'value'; + session_write_close(); + $_SESSION = []; + session_abort(); + + return $sessionId; + } } diff --git a/Tests/EventListener/SurrogateListenerTest.php b/Tests/EventListener/SurrogateListenerTest.php index fc51de252e..88a442a928 100644 --- a/Tests/EventListener/SurrogateListenerTest.php +++ b/Tests/EventListener/SurrogateListenerTest.php @@ -26,7 +26,7 @@ class SurrogateListenerTest extends TestCase public function testFilterDoesNothingForSubRequests() { $dispatcher = new EventDispatcher(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $response = new Response('foo '); $listener = new SurrogateListener(new Esi()); @@ -40,12 +40,12 @@ public function testFilterDoesNothingForSubRequests() public function testFilterWhenThereIsSomeEsiIncludes() { $dispatcher = new EventDispatcher(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $response = new Response('foo '); $listener = new SurrogateListener(new Esi()); $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'onKernelResponse']); - $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('content="ESI/1.0"', $event->getResponse()->headers->get('Surrogate-Control')); @@ -54,12 +54,12 @@ public function testFilterWhenThereIsSomeEsiIncludes() public function testFilterWhenThereIsNoEsiIncludes() { $dispatcher = new EventDispatcher(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $response = new Response('foo'); $listener = new SurrogateListener(new Esi()); $dispatcher->addListener(KernelEvents::RESPONSE, [$listener, 'onKernelResponse']); - $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('', $event->getResponse()->headers->get('Surrogate-Control')); diff --git a/Tests/EventListener/TestSessionListenerTest.php b/Tests/EventListener/TestSessionListenerTest.php index 1d9ea37977..3bb7697062 100644 --- a/Tests/EventListener/TestSessionListenerTest.php +++ b/Tests/EventListener/TestSessionListenerTest.php @@ -14,10 +14,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\EventListener\SessionListener; +use Symfony\Component\HttpKernel\EventListener\AbstractTestSessionListener; use Symfony\Component\HttpKernel\EventListener\TestSessionListener; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -27,6 +28,7 @@ * Tests SessionListener. * * @author Bulat Shakirzyanov + * @group legacy */ class TestSessionListenerTest extends TestCase { @@ -42,14 +44,14 @@ class TestSessionListenerTest extends TestCase protected function setUp(): void { - $this->listener = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\EventListener\AbstractTestSessionListener'); + $this->listener = $this->getMockForAbstractClass(AbstractTestSessionListener::class); $this->session = $this->getSession(); $this->listener->expects($this->any()) ->method('getSession') ->willReturn($this->session); } - public function testShouldSaveMasterRequestSession() + public function testShouldSaveMainRequestSession() { $this->sessionHasBeenStarted(); $this->sessionMustBeSaved(); @@ -70,7 +72,7 @@ public function testDoesNotDeleteCookieIfUsingSessionLifetime() @ini_set('session.cookie_lifetime', 0); - $response = $this->filterResponse(new Request(), HttpKernelInterface::MASTER_REQUEST); + $response = $this->filterResponse(new Request(), HttpKernelInterface::MAIN_REQUEST); $cookies = $response->headers->getCookies(); $this->assertEquals(0, reset($cookies)->getExpiresTime()); @@ -84,7 +86,7 @@ public function testEmptySessionDoesNotSendCookie() $this->sessionHasBeenStarted(); $this->sessionIsEmpty(); - $response = $this->filterResponse(new Request(), HttpKernelInterface::MASTER_REQUEST); + $response = $this->filterResponse(new Request(), HttpKernelInterface::MAIN_REQUEST); $this->assertSame([], $response->headers->getCookies()); } @@ -95,12 +97,13 @@ public function testEmptySessionWithNewSessionIdDoesSendCookie() $this->sessionIsEmpty(); $this->fixSessionId('456'); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('/', 'GET', [], ['MOCKSESSID' => '123']); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + $request->setSession($this->getSession()); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->listener->onKernelRequest($event); - $response = $this->filterResponse(new Request(), HttpKernelInterface::MASTER_REQUEST); + $response = $this->filterResponse(new Request(), HttpKernelInterface::MAIN_REQUEST); $this->assertNotEmpty($response->headers->getCookies()); } @@ -114,14 +117,15 @@ public function testSessionWithNewSessionIdAndNewCookieDoesNotSendAnotherCookie( $this->sessionIsEmpty(); $this->fixSessionId('456'); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = Request::create('/', 'GET', [], ['MOCKSESSID' => '123']); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + $request->setSession($this->getSession()); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $this->listener->onKernelRequest($event); $response = new Response('', 200, ['Set-Cookie' => $existing]); - $response = $this->filterResponse(new Request(), HttpKernelInterface::MASTER_REQUEST, $response); + $response = $this->filterResponse(new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $this->assertSame($expected, $response->headers->all()['set-cookie']); } @@ -145,19 +149,19 @@ public function testUnstartedSessionIsNotSave() public function testDoesNotThrowIfRequestDoesNotHaveASession() { - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); - $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, new Response()); + $kernel = $this->createMock(HttpKernelInterface::class); + $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, new Response()); $this->listener->onKernelResponse($event); $this->assertTrue(true); } - private function filterResponse(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, Response $response = null) + private function filterResponse(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, Response $response = null) { $request->setSession($this->session); - $response = $response ?: new Response(); - $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $response = $response ?? new Response(); + $kernel = $this->createMock(HttpKernelInterface::class); $event = new ResponseEvent($kernel, $request, $type, $response); $this->listener->onKernelResponse($event); @@ -209,11 +213,7 @@ private function fixSessionId($sessionId) private function getSession() { - $mock = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session') - ->disableOriginalConstructor() - ->getMock(); - - // set return value for getName() + $mock = $this->createMock(Session::class); $mock->expects($this->any())->method('getName')->willReturn('MOCKSESSID'); return $mock; diff --git a/Tests/EventListener/ValidateRequestListenerTest.php b/Tests/EventListener/ValidateRequestListenerTest.php index 7cec68143b..5b02995dc3 100644 --- a/Tests/EventListener/ValidateRequestListenerTest.php +++ b/Tests/EventListener/ValidateRequestListenerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; @@ -26,11 +27,11 @@ protected function tearDown(): void Request::setTrustedProxies([], -1); } - public function testListenerThrowsWhenMasterRequestHasInconsistentClientIps() + public function testListenerThrowsWhenMainRequestHasInconsistentClientIps() { - $this->expectException('Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException'); + $this->expectException(ConflictingHeadersException::class); $dispatcher = new EventDispatcher(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $request->setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_FORWARDED); @@ -39,7 +40,7 @@ public function testListenerThrowsWhenMasterRequestHasInconsistentClientIps() $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); $dispatcher->addListener(KernelEvents::REQUEST, [new ValidateRequestListener(), 'onKernelRequest']); - $event = new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $dispatcher->dispatch($event, KernelEvents::REQUEST); } diff --git a/Tests/Exception/AccessDeniedHttpExceptionTest.php b/Tests/Exception/AccessDeniedHttpExceptionTest.php index 3a34cc47bc..a810255b1e 100644 --- a/Tests/Exception/AccessDeniedHttpExceptionTest.php +++ b/Tests/Exception/AccessDeniedHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; class AccessDeniedHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new AccessDeniedHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/BadRequestHttpExceptionTest.php b/Tests/Exception/BadRequestHttpExceptionTest.php index 462fd9cb1d..2e09653fa7 100644 --- a/Tests/Exception/BadRequestHttpExceptionTest.php +++ b/Tests/Exception/BadRequestHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; class BadRequestHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new BadRequestHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/ConflictHttpExceptionTest.php b/Tests/Exception/ConflictHttpExceptionTest.php index 760600a10f..dbab2acff5 100644 --- a/Tests/Exception/ConflictHttpExceptionTest.php +++ b/Tests/Exception/ConflictHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; class ConflictHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new ConflictHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/GoneHttpExceptionTest.php b/Tests/Exception/GoneHttpExceptionTest.php index 30dafe4922..2582ab71b3 100644 --- a/Tests/Exception/GoneHttpExceptionTest.php +++ b/Tests/Exception/GoneHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\GoneHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; class GoneHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new GoneHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/HttpExceptionTest.php b/Tests/Exception/HttpExceptionTest.php index a9431f4b5a..feaec807fd 100644 --- a/Tests/Exception/HttpExceptionTest.php +++ b/Tests/Exception/HttpExceptionTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; use PHPUnit\Framework\TestCase; @@ -32,7 +41,7 @@ public function testHeadersDefault() */ public function testHeadersConstructor($headers) { - $exception = new HttpException(200, null, null, $headers); + $exception = new HttpException(200, '', null, $headers); $this->assertSame($headers, $exception->getHeaders()); } @@ -50,11 +59,11 @@ public function testThrowableIsAllowedForPrevious() { $previous = new class('Error of PHP 7+') extends \Error { }; - $exception = $this->createException(null, $previous); + $exception = $this->createException('', $previous); $this->assertSame($previous, $exception->getPrevious()); } - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new HttpException(200, $message, $previous, $headers, $code); } diff --git a/Tests/Exception/LengthRequiredHttpExceptionTest.php b/Tests/Exception/LengthRequiredHttpExceptionTest.php index 8676d67238..5525870e1e 100644 --- a/Tests/Exception/LengthRequiredHttpExceptionTest.php +++ b/Tests/Exception/LengthRequiredHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException; class LengthRequiredHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new LengthRequiredHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/MethodNotAllowedHttpExceptionTest.php b/Tests/Exception/MethodNotAllowedHttpExceptionTest.php index efb0b50caf..61ecb84da4 100644 --- a/Tests/Exception/MethodNotAllowedHttpExceptionTest.php +++ b/Tests/Exception/MethodNotAllowedHttpExceptionTest.php @@ -1,7 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; class MethodNotAllowedHttpExceptionTest extends HttpExceptionTest @@ -18,7 +28,7 @@ public function testWithHeaderConstruct() 'Cache-Control' => 'public, s-maxage=1200', ]; - $exception = new MethodNotAllowedHttpException(['get'], null, null, null, $headers); + $exception = new MethodNotAllowedHttpException(['get'], '', null, 0, $headers); $headers['Allow'] = 'GET'; @@ -35,7 +45,7 @@ public function testHeadersSetter($headers) $this->assertSame($headers, $exception->getHeaders()); } - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new MethodNotAllowedHttpException(['get'], $message, $previous, $code, $headers); } diff --git a/Tests/Exception/NotAcceptableHttpExceptionTest.php b/Tests/Exception/NotAcceptableHttpExceptionTest.php index 021c69e289..6df823ada0 100644 --- a/Tests/Exception/NotAcceptableHttpExceptionTest.php +++ b/Tests/Exception/NotAcceptableHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; class NotAcceptableHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new NotAcceptableHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/NotFoundHttpExceptionTest.php b/Tests/Exception/NotFoundHttpExceptionTest.php index 0bf369b1a0..8152a727fd 100644 --- a/Tests/Exception/NotFoundHttpExceptionTest.php +++ b/Tests/Exception/NotFoundHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class NotFoundHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new NotFoundHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/PreconditionFailedHttpExceptionTest.php b/Tests/Exception/PreconditionFailedHttpExceptionTest.php index 04d79c499d..d215792875 100644 --- a/Tests/Exception/PreconditionFailedHttpExceptionTest.php +++ b/Tests/Exception/PreconditionFailedHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; class PreconditionFailedHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new PreconditionFailedHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/PreconditionRequiredHttpExceptionTest.php b/Tests/Exception/PreconditionRequiredHttpExceptionTest.php index 82076617a8..452b226c49 100644 --- a/Tests/Exception/PreconditionRequiredHttpExceptionTest.php +++ b/Tests/Exception/PreconditionRequiredHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException; class PreconditionRequiredHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new PreconditionRequiredHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/ServiceUnavailableHttpExceptionTest.php b/Tests/Exception/ServiceUnavailableHttpExceptionTest.php index fac197c852..4f0aa3a458 100644 --- a/Tests/Exception/ServiceUnavailableHttpExceptionTest.php +++ b/Tests/Exception/ServiceUnavailableHttpExceptionTest.php @@ -1,7 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; class ServiceUnavailableHttpExceptionTest extends HttpExceptionTest @@ -18,7 +28,7 @@ public function testWithHeaderConstruct() 'Cache-Control' => 'public, s-maxage=1337', ]; - $exception = new ServiceUnavailableHttpException(1337, null, null, null, $headers); + $exception = new ServiceUnavailableHttpException(1337, '', null, 0, $headers); $headers['Retry-After'] = 1337; @@ -35,7 +45,7 @@ public function testHeadersSetter($headers) $this->assertSame($headers, $exception->getHeaders()); } - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new ServiceUnavailableHttpException(null, $message, $previous, $code, $headers); } diff --git a/Tests/Exception/TooManyRequestsHttpExceptionTest.php b/Tests/Exception/TooManyRequestsHttpExceptionTest.php index 8b59e9894a..4dc2e41ea5 100644 --- a/Tests/Exception/TooManyRequestsHttpExceptionTest.php +++ b/Tests/Exception/TooManyRequestsHttpExceptionTest.php @@ -1,7 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class TooManyRequestsHttpExceptionTest extends HttpExceptionTest @@ -18,7 +28,7 @@ public function testWithHeaderConstruct() 'Cache-Control' => 'public, s-maxage=69', ]; - $exception = new TooManyRequestsHttpException(69, null, null, null, $headers); + $exception = new TooManyRequestsHttpException(69, '', null, 0, $headers); $headers['Retry-After'] = 69; @@ -35,7 +45,7 @@ public function testHeadersSetter($headers) $this->assertSame($headers, $exception->getHeaders()); } - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new TooManyRequestsHttpException(null, $message, $previous, $code, $headers); } diff --git a/Tests/Exception/UnauthorizedHttpExceptionTest.php b/Tests/Exception/UnauthorizedHttpExceptionTest.php index 92d427b6e4..dda2777c91 100644 --- a/Tests/Exception/UnauthorizedHttpExceptionTest.php +++ b/Tests/Exception/UnauthorizedHttpExceptionTest.php @@ -1,7 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; class UnauthorizedHttpExceptionTest extends HttpExceptionTest @@ -18,7 +28,7 @@ public function testWithHeaderConstruct() 'Cache-Control' => 'public, s-maxage=1200', ]; - $exception = new UnauthorizedHttpException('Challenge', null, null, null, $headers); + $exception = new UnauthorizedHttpException('Challenge', '', null, 0, $headers); $headers['WWW-Authenticate'] = 'Challenge'; @@ -35,7 +45,7 @@ public function testHeadersSetter($headers) $this->assertSame($headers, $exception->getHeaders()); } - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new UnauthorizedHttpException('Challenge', $message, $previous, $code, $headers); } diff --git a/Tests/Exception/UnprocessableEntityHttpExceptionTest.php b/Tests/Exception/UnprocessableEntityHttpExceptionTest.php index ffa4e177ee..8b4ece20ee 100644 --- a/Tests/Exception/UnprocessableEntityHttpExceptionTest.php +++ b/Tests/Exception/UnprocessableEntityHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; class UnprocessableEntityHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new UnprocessableEntityHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php b/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php index fa28bbd19b..0295d61e0a 100644 --- a/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php +++ b/Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php @@ -1,12 +1,22 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\HttpKernel\Tests\Exception; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; class UnsupportedMediaTypeHttpExceptionTest extends HttpExceptionTest { - protected function createException(string $message = null, \Throwable $previous = null, ?int $code = 0, array $headers = []) + protected function createException(string $message = '', \Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new UnsupportedMediaTypeHttpException($message, $previous, $code, $headers); } diff --git a/Tests/Fixtures/Attribute/Foo.php b/Tests/Fixtures/Attribute/Foo.php new file mode 100644 index 0000000000..e01a5a6e8d --- /dev/null +++ b/Tests/Fixtures/Attribute/Foo.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; + +use Symfony\Component\HttpKernel\Attribute\ArgumentInterface; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class Foo +{ + private $foo; + + public function __construct($foo) + { + $this->foo = $foo; + } +} diff --git a/Tests/Fixtures/Controller/AttributeController.php b/Tests/Fixtures/Controller/AttributeController.php new file mode 100644 index 0000000000..915b5e41d4 --- /dev/null +++ b/Tests/Fixtures/Controller/AttributeController.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; + +use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; + +class AttributeController +{ + public function action(#[Foo('bar')] string $baz) { + } + + public function multiAttributeArg(#[Foo('bar'), Undefined('bar')] string $baz) { + } + + public function issue41478(#[Foo('bar')] string $baz, string $bat) { + } +} diff --git a/Tests/Fixtures/KernelForTest.php b/Tests/Fixtures/KernelForTest.php index f3b1951b83..48d4484fa8 100644 --- a/Tests/Fixtures/KernelForTest.php +++ b/Tests/Fixtures/KernelForTest.php @@ -12,14 +12,23 @@ namespace Symfony\Component\HttpKernel\Tests\Fixtures; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\Kernel; class KernelForTest extends Kernel { + private $fakeContainer; + + public function __construct(string $environment, bool $debug, bool $fakeContainer = true) + { + parent::__construct($environment, $debug); + $this->fakeContainer = $fakeContainer; + } + public function getBundleMap() { - return $this->bundleMap; + return []; } public function registerBundles(): iterable @@ -40,4 +49,13 @@ public function getProjectDir(): string { return __DIR__; } + + protected function initializeContainer() + { + if ($this->fakeContainer) { + $this->container = new ContainerBuilder(); + } else { + parent::initializeContainer(); + } + } } diff --git a/Tests/Fixtures/Suit.php b/Tests/Fixtures/Suit.php new file mode 100644 index 0000000000..5d9623b225 --- /dev/null +++ b/Tests/Fixtures/Suit.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures; + +enum Suit: string +{ + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} diff --git a/Tests/Fragment/EsiFragmentRendererTest.php b/Tests/Fragment/EsiFragmentRendererTest.php index df74ade154..3817f4897e 100644 --- a/Tests/Fragment/EsiFragmentRendererTest.php +++ b/Tests/Fragment/EsiFragmentRendererTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\UriSigner; @@ -67,7 +68,7 @@ public function testRenderControllerReference() public function testRenderControllerReferenceWithoutSignerThrowsException() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); $request = Request::create('/'); @@ -79,7 +80,7 @@ public function testRenderControllerReferenceWithoutSignerThrowsException() public function testRenderAltControllerReferenceWithoutSignerThrowsException() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); $request = Request::create('/'); @@ -91,7 +92,7 @@ public function testRenderAltControllerReferenceWithoutSignerThrowsException() private function getInlineStrategy($called = false) { - $inline = $this->getMockBuilder('Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer')->disableOriginalConstructor()->getMock(); + $inline = $this->createMock(InlineFragmentRenderer::class); if ($called) { $inline->expects($this->once())->method('render'); diff --git a/Tests/Fragment/FragmentHandlerTest.php b/Tests/Fragment/FragmentHandlerTest.php index 15e543a214..1d0eb90bf6 100644 --- a/Tests/Fragment/FragmentHandlerTest.php +++ b/Tests/Fragment/FragmentHandlerTest.php @@ -13,22 +13,19 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; +use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; -/** - * @group time-sensitive - */ class FragmentHandlerTest extends TestCase { private $requestStack; protected function setUp(): void { - $this->requestStack = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack') - ->disableOriginalConstructor() - ->getMock() - ; + $this->requestStack = $this->createMock(RequestStack::class); $this->requestStack ->expects($this->any()) ->method('getCurrentRequest') @@ -38,14 +35,14 @@ protected function setUp(): void public function testRenderWhenRendererDoesNotExist() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $handler = new FragmentHandler($this->requestStack); $handler->render('/', 'foo'); } public function testRenderWithUnknownRenderer() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $handler = $this->getHandler($this->returnValue(new Response('foo'))); $handler->render('/', 'bar'); @@ -53,23 +50,45 @@ public function testRenderWithUnknownRenderer() public function testDeliverWithUnsuccessfulResponse() { - $this->expectException('RuntimeException'); - $this->expectExceptionMessage('Error when rendering "http://localhost/" (Status code is 404).'); $handler = $this->getHandler($this->returnValue(new Response('foo', 404))); - - $handler->render('/', 'foo'); + try { + $handler->render('/', 'foo'); + $this->fail('->render() throws a \RuntimeException exception if response is not successful'); + } catch (\Exception $e) { + $this->assertInstanceOf(\RuntimeException::class, $e); + $this->assertEquals(0, $e->getCode()); + $this->assertEquals('Error when rendering "http://localhost/" (Status code is 404).', $e->getMessage()); + + $previousException = $e->getPrevious(); + $this->assertInstanceOf(HttpException::class, $previousException); + $this->assertEquals(404, $previousException->getStatusCode()); + $this->assertEquals(0, $previousException->getCode()); + } } public function testRender() { - $handler = $this->getHandler($this->returnValue(new Response('foo')), ['/', Request::create('/'), ['foo' => 'foo', 'ignore_errors' => true]]); + $expectedRequest = Request::create('/'); + $handler = $this->getHandler( + $this->returnValue(new Response('foo')), + [ + '/', + $this->callback(function (Request $request) use ($expectedRequest) { + $expectedRequest->server->remove('REQUEST_TIME_FLOAT'); + $request->server->remove('REQUEST_TIME_FLOAT'); + + return $expectedRequest == $request; + }), + ['foo' => 'foo', 'ignore_errors' => true], + ] + ); $this->assertEquals('foo', $handler->render('/', 'foo', ['foo' => 'foo'])); } protected function getHandler($returnValue, $arguments = []) { - $renderer = $this->getMockBuilder('Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface')->getMock(); + $renderer = $this->createMock(FragmentRendererInterface::class); $renderer ->expects($this->any()) ->method('getName') diff --git a/Tests/Fragment/HIncludeFragmentRendererTest.php b/Tests/Fragment/HIncludeFragmentRendererTest.php index 87587c8009..2dd09aca3e 100644 --- a/Tests/Fragment/HIncludeFragmentRendererTest.php +++ b/Tests/Fragment/HIncludeFragmentRendererTest.php @@ -23,7 +23,7 @@ class HIncludeFragmentRendererTest extends TestCase { public function testRenderExceptionWhenControllerAndNoSigner() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $strategy = new HIncludeFragmentRenderer(); $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/')); } diff --git a/Tests/Fragment/InlineFragmentRendererTest.php b/Tests/Fragment/InlineFragmentRendererTest.php index a064a76c7d..69bd7445ac 100644 --- a/Tests/Fragment/InlineFragmentRendererTest.php +++ b/Tests/Fragment/InlineFragmentRendererTest.php @@ -16,13 +16,19 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +/** + * @group time-sensitive + */ class InlineFragmentRendererTest extends TestCase { public function testRender() @@ -71,8 +77,8 @@ public function testRenderWithTrustedHeaderDisabled() public function testRenderExceptionNoIgnoreErrors() { - $this->expectException('RuntimeException'); - $dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock(); + $this->expectException(\RuntimeException::class); + $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher->expects($this->never())->method('dispatch'); $strategy = new InlineFragmentRenderer($this->getKernel($this->throwException(new \RuntimeException('foo'))), $dispatcher); @@ -86,7 +92,7 @@ public function testRenderExceptionIgnoreErrors() $kernel = $this->getKernel($this->throwException($exception)); $request = Request::create('/'); $expectedEvent = new ExceptionEvent($kernel, $request, $kernel::SUB_REQUEST, $exception); - $dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock(); + $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher->expects($this->once())->method('dispatch')->with($expectedEvent, KernelEvents::EXCEPTION); $strategy = new InlineFragmentRenderer($kernel, $dispatcher); @@ -106,7 +112,7 @@ public function testRenderExceptionIgnoreErrorsWithAlt() private function getKernel($returnValue) { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->any()) ->method('handle') @@ -118,7 +124,7 @@ private function getKernel($returnValue) public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() { - $controllerResolver = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface')->getMock(); + $controllerResolver = $this->createMock(ControllerResolverInterface::class); $controllerResolver ->expects($this->once()) ->method('getController') @@ -129,7 +135,7 @@ public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() }) ; - $argumentResolver = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface')->getMock(); + $argumentResolver = $this->createMock(ArgumentResolverInterface::class); $argumentResolver ->expects($this->once()) ->method('getArguments') @@ -149,7 +155,7 @@ public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() $this->assertEquals('Foo', ob_get_clean()); } - public function testLocaleAndFormatAreIsKeptInSubrequest() + public function testLocaleAndFormatAreKeptInSubrequest() { $expectedSubRequest = Request::create('/'); $expectedSubRequest->attributes->set('_format', 'foo'); @@ -253,16 +259,20 @@ public function testIpAddressOfRangedTrustedProxyIsSetAsRemote() } /** - * Creates a Kernel expecting a request equals to $request - * Allows delta in comparison in case REQUEST_TIME changed by 1 second. + * Creates a Kernel expecting a request equals to $request. */ - private function getKernelExpectingRequest(Request $request, $strict = false) + private function getKernelExpectingRequest(Request $expectedRequest) { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') - ->with($this->equalTo($request, 1)) + ->with($this->callback(function (Request $request) use ($expectedRequest) { + $expectedRequest->server->remove('REQUEST_TIME_FLOAT'); + $request->server->remove('REQUEST_TIME_FLOAT'); + + return $expectedRequest == $request; + })) ->willReturn(new Response('foo')); return $kernel; diff --git a/Tests/Fragment/RoutableFragmentRendererTest.php b/Tests/Fragment/RoutableFragmentRendererTest.php index 151adb0e97..d17643c186 100644 --- a/Tests/Fragment/RoutableFragmentRendererTest.php +++ b/Tests/Fragment/RoutableFragmentRendererTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer; class RoutableFragmentRendererTest extends TestCase { @@ -60,7 +61,7 @@ public function testGenerateFragmentUriWithARequest() */ public function testGenerateFragmentUriWithNonScalar($controller) { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $this->callGenerateFragmentUriMethod($controller, Request::create('/')); } @@ -74,7 +75,7 @@ public function getGenerateFragmentUriDataWithNonScalar() private function callGenerateFragmentUriMethod(ControllerReference $reference, Request $request, $absolute = false) { - $renderer = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer'); + $renderer = $this->getMockForAbstractClass(RoutableFragmentRenderer::class); $r = new \ReflectionObject($renderer); $m = $r->getMethod('generateFragmentUri'); $m->setAccessible(true); diff --git a/Tests/Fragment/SsiFragmentRendererTest.php b/Tests/Fragment/SsiFragmentRendererTest.php index df30e67727..63a92028f4 100644 --- a/Tests/Fragment/SsiFragmentRendererTest.php +++ b/Tests/Fragment/SsiFragmentRendererTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; use Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer; use Symfony\Component\HttpKernel\HttpCache\Ssi; use Symfony\Component\HttpKernel\UriSigner; @@ -58,7 +59,7 @@ public function testRenderControllerReference() public function testRenderControllerReferenceWithoutSignerThrowsException() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy()); $request = Request::create('/'); @@ -70,7 +71,7 @@ public function testRenderControllerReferenceWithoutSignerThrowsException() public function testRenderAltControllerReferenceWithoutSignerThrowsException() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy()); $request = Request::create('/'); @@ -82,7 +83,7 @@ public function testRenderAltControllerReferenceWithoutSignerThrowsException() private function getInlineStrategy($called = false) { - $inline = $this->getMockBuilder('Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer')->disableOriginalConstructor()->getMock(); + $inline = $this->createMock(InlineFragmentRenderer::class); if ($called) { $inline->expects($this->once())->method('render'); diff --git a/Tests/HttpCache/EsiTest.php b/Tests/HttpCache/EsiTest.php index cdf729e331..32bd3ac7b5 100644 --- a/Tests/HttpCache/EsiTest.php +++ b/Tests/HttpCache/EsiTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\Esi; +use Symfony\Component\HttpKernel\HttpCache\HttpCache; class EsiTest extends TestCase { @@ -155,7 +156,7 @@ public function testProcessEscapesPhpTags() public function testProcessWhenNoSrcInAnEsi() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $esi = new Esi(); $request = Request::create('/'); @@ -193,7 +194,7 @@ public function testHandle() public function testHandleWhenResponseIsNot200() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $esi = new Esi(); $response = new Response('foo'); $response->setStatusCode(404); @@ -220,9 +221,18 @@ public function testHandleWhenResponseIsNot200AndAltIsPresent() $this->assertEquals('bar', $esi->handle($cache, '/', '/alt', false)); } + public function testHandleWhenResponseIsNotModified() + { + $esi = new Esi(); + $response = new Response(''); + $response->setStatusCode(304); + $cache = $this->getCache(Request::create('/'), $response); + $this->assertEquals('', $esi->handle($cache, '/', '/alt', true)); + } + protected function getCache($request, $response) { - $cache = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpCache\HttpCache')->setMethods(['getRequest', 'handle'])->disableOriginalConstructor()->getMock(); + $cache = $this->getMockBuilder(HttpCache::class)->setMethods(['getRequest', 'handle'])->disableOriginalConstructor()->getMock(); $cache->expects($this->any()) ->method('getRequest') ->willReturn($request) diff --git a/Tests/HttpCache/HttpCacheTest.php b/Tests/HttpCache/HttpCacheTest.php index 802f1d1007..7d8ef79187 100644 --- a/Tests/HttpCache/HttpCacheTest.php +++ b/Tests/HttpCache/HttpCacheTest.php @@ -16,7 +16,9 @@ use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\HttpCache\StoreInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Kernel; /** * @group time-sensitive @@ -25,7 +27,7 @@ class HttpCacheTest extends HttpCacheTestCase { public function testTerminateDelegatesTerminationOnlyForTerminableInterface() { - $storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface') + $storeMock = $this->getMockBuilder(StoreInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -37,7 +39,7 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface() $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface'); // implements TerminableInterface - $kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel') + $kernelMock = $this->getMockBuilder(Kernel::class) ->disableOriginalConstructor() ->setMethods(['terminate', 'registerBundles', 'registerContainerConfiguration']) ->getMock(); @@ -129,8 +131,8 @@ public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified() { $time = \DateTime::createFromFormat('U', time()); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822), 'Content-Type' => 'text/plain'], 'Hello World'); - $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Last-Modified' => $time->format(\DATE_RFC2822), 'Content-Type' => 'text/plain'], 'Hello World'); + $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); @@ -154,31 +156,31 @@ public function testRespondsWith304WhenIfNoneMatchMatchesETag() $this->assertTraceContains('store'); } - public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch() + public function testRespondsWith304WhenIfNoneMatchAndIfModifiedSinceBothMatch() { $time = \DateTime::createFromFormat('U', time()); $this->setNextResponse(200, [], '', function ($request, $response) use ($time) { $response->setStatusCode(200); $response->headers->set('ETag', '12345'); - $response->headers->set('Last-Modified', $time->format(DATE_RFC2822)); + $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); $response->headers->set('Content-Type', 'text/plain'); $response->setContent('Hello World'); }); // only ETag matches $t = \DateTime::createFromFormat('U', time() - 3600); - $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(DATE_RFC2822)]); + $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); - $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertEquals(304, $this->response->getStatusCode()); // only Last-Modified matches - $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)]); + $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); // Both matches - $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)]); + $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); } @@ -210,8 +212,8 @@ public function testIncrementsMaxAgeWhenNoDateIsSpecifiedEventWhenUsingETag() public function testValidatesPrivateResponsesCachedOnTheClient() { - $this->setNextResponse(200, [], '', function ($request, $response) { - $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH')); + $this->setNextResponse(200, [], '', function (Request $request, $response) { + $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH', '')); if ($request->cookies->has('authenticated')) { $response->headers->set('Cache-Control', 'private, no-store'); $response->setETag('"private tag"'); @@ -257,7 +259,7 @@ public function testStoresResponsesWhenNoCacheRequestDirectivePresent() { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'no-cache']); $this->assertHttpKernelIsCalled(); @@ -393,7 +395,7 @@ public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIs public function testFetchesResponseFromBackendWhenCacheMisses() { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); @@ -405,7 +407,7 @@ public function testDoesNotCacheSomeStatusCodeResponses() { foreach (array_merge(range(201, 202), range(204, 206), range(303, 305), range(400, 403), range(405, 409), range(411, 417), range(500, 505)) as $code) { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse($code, ['Expires' => $time->format(DATE_RFC2822)]); + $this->setNextResponse($code, ['Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals($code, $this->response->getStatusCode()); @@ -417,7 +419,7 @@ public function testDoesNotCacheSomeStatusCodeResponses() public function testDoesNotCacheResponsesWithExplicitNoStoreDirective() { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'no-store']); + $this->setNextResponse(200, ['Expires' => $time->format(\DATE_RFC2822), 'Cache-Control' => 'no-store']); $this->request('GET', '/'); $this->assertTraceNotContains('store'); @@ -436,7 +438,7 @@ public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator public function testCachesResponsesWithExplicitNoCacheDirective() { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, no-cache']); + $this->setNextResponse(200, ['Expires' => $time->format(\DATE_RFC2822), 'Cache-Control' => 'public, no-cache']); $this->request('GET', '/'); $this->assertTraceContains('store'); @@ -462,7 +464,7 @@ public function testRevalidatesResponsesWithNoCacheDirectiveEvenIfFresh() public function testCachesResponsesWithAnExpirationHeader() { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); @@ -511,7 +513,7 @@ public function testCachesResponsesWithASMaxAgeDirective() public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation() { $time = \DateTime::createFromFormat('U', time()); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Last-Modified' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); @@ -535,7 +537,7 @@ public function testHitsCachedResponsesWithExpiresHeader() { $time1 = \DateTime::createFromFormat('U', time() - 5); $time2 = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Date' => $time1->format(DATE_RFC2822), 'Expires' => $time2->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Date' => $time1->format(\DATE_RFC2822), 'Expires' => $time2->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); @@ -559,7 +561,7 @@ public function testHitsCachedResponsesWithExpiresHeader() public function testHitsCachedResponseWithMaxAgeDirective() { $time = \DateTime::createFromFormat('U', time() - 5); - $this->setNextResponse(200, ['Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, max-age=10']); + $this->setNextResponse(200, ['Date' => $time->format(\DATE_RFC2822), 'Cache-Control' => 'public, max-age=10']); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); @@ -588,7 +590,7 @@ public function testDegradationWhenCacheLocked() $this->cacheConfig['stale_while_revalidate'] = 10; - // The prescence of Last-Modified makes this cacheable (because Response::isValidateable() then). + // The presence of Last-Modified makes this cacheable (because Response::isValidateable() then). $this->setNextResponse(200, ['Cache-Control' => 'public, s-maxage=5', 'Last-Modified' => 'some while ago'], 'Old response'); $this->request('GET', '/'); // warm the cache @@ -623,7 +625,7 @@ public function testDegradationWhenCacheLocked() public function testHitsCachedResponseWithSMaxAgeDirective() { $time = \DateTime::createFromFormat('U', time() - 5); - $this->setNextResponse(200, ['Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0']); + $this->setNextResponse(200, ['Date' => $time->format(\DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0']); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); @@ -691,7 +693,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertCount(1, $values); $tmp = unserialize($values[0]); $time = \DateTime::createFromFormat('U', time() - 5); - $tmp[0][1]['date'] = $time->format(DATE_RFC2822); + $tmp[0][1]['date'] = $time->format(\DATE_RFC2822); $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->setAccessible(true); @@ -741,7 +743,7 @@ public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAft $this->assertCount(1, $values); $tmp = unserialize($values[0]); $time = \DateTime::createFromFormat('U', time() - 5); - $tmp[0][1]['date'] = $time->format(DATE_RFC2822); + $tmp[0][1]['date'] = $time->format(\DATE_RFC2822); $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->setAccessible(true); @@ -783,7 +785,7 @@ public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirectiv public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent() { $time = \DateTime::createFromFormat('U', time() + 5); - $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)]); + $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); // build initial request $this->request('GET', '/'); @@ -801,7 +803,7 @@ public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent() $this->assertCount(1, $values); $tmp = unserialize($values[0]); $time = \DateTime::createFromFormat('U', time()); - $tmp[0][1]['expires'] = $time->format(DATE_RFC2822); + $tmp[0][1]['expires'] = $time->format(\DATE_RFC2822); $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->setAccessible(true); @@ -825,8 +827,8 @@ public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInform $time = \DateTime::createFromFormat('U', time()); $this->setNextResponse(200, [], 'Hello World', function ($request, $response) use ($time) { $response->headers->set('Cache-Control', 'public'); - $response->headers->set('Last-Modified', $time->format(DATE_RFC2822)); - if ($time->format(DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) { + $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); + if ($time->format(\DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) { $response->setStatusCode(304); $response->setContent(''); } @@ -912,7 +914,7 @@ public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInform $this->setNextResponse(200, [], 'Hello World', function (Request $request, Response $response) use ($time) { $response->setSharedMaxAge(10); - $response->headers->set('Last-Modified', $time->format(DATE_RFC2822)); + $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); }); // prime the cache @@ -929,7 +931,7 @@ public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInform sleep(15); // expire the cache $this->setNextResponse(304, [], '', function (Request $request, Response $response) use ($time) { - $this->assertEquals($time->format(DATE_RFC2822), $request->headers->get('IF_MODIFIED_SINCE')); + $this->assertEquals($time->format(\DATE_RFC2822), $request->headers->get('IF_MODIFIED_SINCE')); }); $this->request('GET', '/'); @@ -945,7 +947,7 @@ public function testReplacesCachedResponsesWhenValidationResultsInNon304Response $time = \DateTime::createFromFormat('U', time()); $count = 0; $this->setNextResponse(200, [], 'Hello World', function ($request, $response) use ($time, &$count) { - $response->headers->set('Last-Modified', $time->format(DATE_RFC2822)); + $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); $response->headers->set('Cache-Control', 'public'); switch (++$count) { case 1: @@ -1017,14 +1019,14 @@ public function testSendsNoContentWhenFresh() $time = \DateTime::createFromFormat('U', time()); $this->setNextResponse(200, [], 'Hello World', function ($request, $response) use ($time) { $response->headers->set('Cache-Control', 'public, max-age=10'); - $response->headers->set('Last-Modified', $time->format(DATE_RFC2822)); + $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); }); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals('Hello World', $this->response->getContent()); - $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)]); + $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(304, $this->response->getStatusCode()); $this->assertEquals('', $this->response->getContent()); @@ -1207,7 +1209,7 @@ public function testEsiCacheSendsTheLowestTtlForHeadRequests() $responses = [ [ 'status' => 200, - 'body' => 'I am a long-lived master response, but I embed a short-lived resource: ', + 'body' => 'I am a long-lived main response, but I embed a short-lived resource: ', 'headers' => [ 'Cache-Control' => 's-maxage=300', 'Surrogate-Control' => 'content="ESI/1.0"', @@ -1265,7 +1267,7 @@ public function testEsiCacheForceValidationForHeadRequests() $responses = [ [ 'status' => 200, - 'body' => 'I am the master response and use expiration caching, but I embed another resource: ', + 'body' => 'I am the main response and use expiration caching, but I embed another resource: ', 'headers' => [ 'Cache-Control' => 's-maxage=300', 'Surrogate-Control' => 'content="ESI/1.0"', @@ -1361,7 +1363,7 @@ public function testClientIpIsAlwaysLocalhostForForwardedRequests() */ public function testHttpCacheIsSetAsATrustedProxy(array $existing) { - Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_FOR); $this->setNextResponse(); $this->request('GET', '/', ['REMOTE_ADDR' => '10.0.0.1']); @@ -1425,7 +1427,7 @@ public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses() 'headers' => [ 'Surrogate-Control' => 'content="ESI/1.0"', 'ETag' => 'hey', - 'Last-Modified' => $time->format(DATE_RFC2822), + 'Last-Modified' => $time->format(\DATE_RFC2822), ], ], [ @@ -1453,7 +1455,7 @@ public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadReq 'headers' => [ 'Surrogate-Control' => 'content="ESI/1.0"', 'ETag' => 'hey', - 'Last-Modified' => $time->format(DATE_RFC2822), + 'Last-Modified' => $time->format(\DATE_RFC2822), ], ], [ @@ -1488,8 +1490,8 @@ public function testDoesNotCacheOptionsRequest() public function testUsesOriginalRequestForSurrogate() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); - $store = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpCache\StoreInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); + $store = $this->createMock(StoreInterface::class); $kernel ->expects($this->exactly(2)) @@ -1509,7 +1511,7 @@ public function testUsesOriginalRequestForSurrogate() $request->server->set('REMOTE_ADDR', '10.0.0.1'); // Main request - $cache->handle($request, HttpKernelInterface::MASTER_REQUEST); + $cache->handle($request, HttpKernelInterface::MAIN_REQUEST); // Main request was now modified by HttpCache // The surrogate will ask for the request using $this->cache->getRequest() @@ -1732,7 +1734,7 @@ public function terminate(Request $request, Response $response) $this->terminateCalled = true; } - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response + public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { } } diff --git a/Tests/HttpCache/HttpCacheTestCase.php b/Tests/HttpCache/HttpCacheTestCase.php index a058a15f15..e47631d178 100644 --- a/Tests/HttpCache/HttpCacheTestCase.php +++ b/Tests/HttpCache/HttpCacheTestCase.php @@ -131,7 +131,7 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi = $this->request = Request::create($uri, $method, [], $cookies, [], $server); $this->request->headers->add($headers); - $this->response = $this->cache->handle($this->request, HttpKernelInterface::MASTER_REQUEST, $this->catch); + $this->response = $this->cache->handle($this->request, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->responses[] = $this->response; } diff --git a/Tests/HttpCache/ResponseCacheStrategyTest.php b/Tests/HttpCache/ResponseCacheStrategyTest.php index 22cadf7129..fa0ad5d311 100644 --- a/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -58,7 +58,7 @@ public function testSharedMaxAgeNotSetIfNotSetInAnyEmbeddedRequest() $this->assertFalse($response->headers->hasCacheControlDirective('s-maxage')); } - public function testSharedMaxAgeNotSetIfNotSetInMasterRequest() + public function testSharedMaxAgeNotSetIfNotSetInMainRequest() { $cacheStrategy = new ResponseCacheStrategy(); @@ -76,7 +76,7 @@ public function testSharedMaxAgeNotSetIfNotSetInMasterRequest() $this->assertFalse($response->headers->hasCacheControlDirective('s-maxage')); } - public function testMasterResponseNotCacheableWhenEmbeddedResponseRequiresValidation() + public function testMainResponseNotCacheableWhenEmbeddedResponseRequiresValidation() { $cacheStrategy = new ResponseCacheStrategy(); @@ -84,66 +84,66 @@ public function testMasterResponseNotCacheableWhenEmbeddedResponseRequiresValida $embeddedResponse->setLastModified(new \DateTime()); $cacheStrategy->add($embeddedResponse); - $masterResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); - $cacheStrategy->update($masterResponse); + $mainResponse = new Response(); + $mainResponse->setSharedMaxAge(3600); + $cacheStrategy->update($mainResponse); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('no-cache')); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('must-revalidate')); - $this->assertFalse($masterResponse->isFresh()); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($mainResponse->isFresh()); } - public function testValidationOnMasterResponseIsNotPossibleWhenItContainsEmbeddedResponses() + public function testValidationOnMainResponseIsNotPossibleWhenItContainsEmbeddedResponses() { $cacheStrategy = new ResponseCacheStrategy(); - // This master response uses the "validation" model - $masterResponse = new Response(); - $masterResponse->setLastModified(new \DateTime()); - $masterResponse->setEtag('foo'); + // This main response uses the "validation" model + $mainResponse = new Response(); + $mainResponse->setLastModified(new \DateTime()); + $mainResponse->setEtag('foo'); // Embedded response uses "expiry" model $embeddedResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); + $mainResponse->setSharedMaxAge(3600); $cacheStrategy->add($embeddedResponse); - $cacheStrategy->update($masterResponse); + $cacheStrategy->update($mainResponse); - $this->assertFalse($masterResponse->isValidateable()); - $this->assertFalse($masterResponse->headers->has('Last-Modified')); - $this->assertFalse($masterResponse->headers->has('ETag')); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('no-cache')); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($mainResponse->isValidateable()); + $this->assertFalse($mainResponse->headers->has('Last-Modified')); + $this->assertFalse($mainResponse->headers->has('ETag')); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('must-revalidate')); } - public function testMasterResponseWithValidationIsUnchangedWhenThereIsNoEmbeddedResponse() + public function testMainResponseWithValidationIsUnchangedWhenThereIsNoEmbeddedResponse() { $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setLastModified(new \DateTime()); - $cacheStrategy->update($masterResponse); + $mainResponse = new Response(); + $mainResponse->setLastModified(new \DateTime()); + $cacheStrategy->update($mainResponse); - $this->assertTrue($masterResponse->isValidateable()); + $this->assertTrue($mainResponse->isValidateable()); } - public function testMasterResponseWithExpirationIsUnchangedWhenThereIsNoEmbeddedResponse() + public function testMainResponseWithExpirationIsUnchangedWhenThereIsNoEmbeddedResponse() { $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); - $cacheStrategy->update($masterResponse); + $mainResponse = new Response(); + $mainResponse->setSharedMaxAge(3600); + $cacheStrategy->update($mainResponse); - $this->assertTrue($masterResponse->isFresh()); + $this->assertTrue($mainResponse->isFresh()); } - public function testMasterResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheable() + public function testMainResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheable() { $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); // Public, cacheable + $mainResponse = new Response(); + $mainResponse->setSharedMaxAge(3600); // Public, cacheable /* This response has no validation or expiration information. That makes it uncacheable, it is always stale. @@ -152,19 +152,19 @@ public function testMasterResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheab $this->assertFalse($embeddedResponse->isFresh()); // not fresh, as no lifetime is provided $cacheStrategy->add($embeddedResponse); - $cacheStrategy->update($masterResponse); + $cacheStrategy->update($mainResponse); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('no-cache')); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('must-revalidate')); - $this->assertFalse($masterResponse->isFresh()); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('no-cache')); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('must-revalidate')); + $this->assertFalse($mainResponse->isFresh()); } public function testEmbeddingPrivateResponseMakesMainResponsePrivate() { $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); // public, cacheable + $mainResponse = new Response(); + $mainResponse->setSharedMaxAge(3600); // public, cacheable // The embedded response might for example contain per-user data that remains valid for 60 seconds $embeddedResponse = new Response(); @@ -172,29 +172,29 @@ public function testEmbeddingPrivateResponseMakesMainResponsePrivate() $embeddedResponse->setMaxAge(60); // this would implicitly set "private" as well, but let's be explicit $cacheStrategy->add($embeddedResponse); - $cacheStrategy->update($masterResponse); + $cacheStrategy->update($mainResponse); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('private')); - $this->assertFalse($masterResponse->headers->hasCacheControlDirective('public')); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('private')); + $this->assertFalse($mainResponse->headers->hasCacheControlDirective('public')); } public function testEmbeddingPublicResponseDoesNotMakeMainResponsePublic() { $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setPrivate(); // this is the default, but let's be explicit - $masterResponse->setMaxAge(100); + $mainResponse = new Response(); + $mainResponse->setPrivate(); // this is the default, but let's be explicit + $mainResponse->setMaxAge(100); $embeddedResponse = new Response(); $embeddedResponse->setPublic(); $embeddedResponse->setSharedMaxAge(100); $cacheStrategy->add($embeddedResponse); - $cacheStrategy->update($masterResponse); + $cacheStrategy->update($mainResponse); - $this->assertTrue($masterResponse->headers->hasCacheControlDirective('private')); - $this->assertFalse($masterResponse->headers->hasCacheControlDirective('public')); + $this->assertTrue($mainResponse->headers->hasCacheControlDirective('private')); + $this->assertFalse($mainResponse->headers->hasCacheControlDirective('public')); } public function testResponseIsExiprableWhenEmbeddedResponseCombinesExpiryAndValidation() @@ -206,39 +206,40 @@ public function testResponseIsExiprableWhenEmbeddedResponseCombinesExpiryAndVali */ $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); + $mainResponse = new Response(); + $mainResponse->setSharedMaxAge(3600); $embeddedResponse = new Response(); $embeddedResponse->setSharedMaxAge(60); $embeddedResponse->setEtag('foo'); $cacheStrategy->add($embeddedResponse); - $cacheStrategy->update($masterResponse); + $cacheStrategy->update($mainResponse); - $this->assertSame('60', $masterResponse->headers->getCacheControlDirective('s-maxage')); + $this->assertEqualsWithDelta(60, (int) $mainResponse->headers->getCacheControlDirective('s-maxage'), 1); } - public function testResponseIsExpirableButNotValidateableWhenMasterResponseCombinesExpirationAndValidation() + public function testResponseIsExpirableButNotValidateableWhenMainResponseCombinesExpirationAndValidation() { $cacheStrategy = new ResponseCacheStrategy(); - $masterResponse = new Response(); - $masterResponse->setSharedMaxAge(3600); - $masterResponse->setEtag('foo'); - $masterResponse->setLastModified(new \DateTime()); + $mainResponse = new Response(); + $mainResponse->setSharedMaxAge(3600); + $mainResponse->setEtag('foo'); + $mainResponse->setLastModified(new \DateTime()); $embeddedResponse = new Response(); $embeddedResponse->setSharedMaxAge(60); $cacheStrategy->add($embeddedResponse); - $cacheStrategy->update($masterResponse); + $cacheStrategy->update($mainResponse); - $this->assertSame('60', $masterResponse->headers->getCacheControlDirective('s-maxage')); - $this->assertFalse($masterResponse->isValidateable()); + $this->assertSame('60', $mainResponse->headers->getCacheControlDirective('s-maxage')); + $this->assertFalse($mainResponse->isValidateable()); } /** + * @group time-sensitive * @dataProvider cacheControlMergingProvider */ public function testCacheControlMerging(array $expects, array $master, array $surrogates) @@ -369,13 +370,53 @@ public function cacheControlMergingProvider() ]; yield 'merge max-age and s-maxage' => [ - ['public' => true, 's-maxage' => '60', 'max-age' => null], + ['public' => true, 'max-age' => '60'], ['public' => true, 's-maxage' => 3600], [ ['public' => true, 'max-age' => 60], ], ]; + yield 's-maxage may be set to 0' => [ + ['public' => true, 's-maxage' => '0', 'max-age' => null], + ['public' => true, 's-maxage' => '0'], + [ + ['public' => true, 's-maxage' => '60'], + ], + ]; + + yield 's-maxage may be set to 0, and works independently from maxage' => [ + ['public' => true, 's-maxage' => '0', 'max-age' => '30'], + ['public' => true, 's-maxage' => '0', 'max-age' => '30'], + [ + ['public' => true, 'max-age' => '60'], + ], + ]; + + yield 'public subresponse without lifetime does not remove lifetime for main response' => [ + ['public' => true, 's-maxage' => '30', 'max-age' => null], + ['public' => true, 's-maxage' => '30'], + [ + ['public' => true], + ], + ]; + + yield 'lifetime for subresponse is kept when main response has no lifetime' => [ + ['public' => true, 'max-age' => '30'], + ['public' => true], + [ + ['public' => true, 'max-age' => '30'], + ], + ]; + + yield 's-maxage on the subresponse implies public, so the result is public as well' => [ + ['public' => true, 'max-age' => '10', 's-maxage' => null], + ['public' => true, 'max-age' => '10'], + [ + ['max-age' => '30', 's-maxage' => '20'], + ], + ]; + yield 'result is private when combining private responses' => [ ['no-cache' => false, 'must-revalidate' => false, 'private' => true], ['s-maxage' => 60, 'private' => true], diff --git a/Tests/HttpCache/SsiTest.php b/Tests/HttpCache/SsiTest.php index 3d68052cdc..157c4d7455 100644 --- a/Tests/HttpCache/SsiTest.php +++ b/Tests/HttpCache/SsiTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Ssi; class SsiTest extends TestCase @@ -122,7 +123,7 @@ public function testProcessEscapesPhpTags() public function testProcessWhenNoSrcInAnSsi() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $ssi = new Ssi(); $request = Request::create('/'); @@ -160,7 +161,7 @@ public function testHandle() public function testHandleWhenResponseIsNot200() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $ssi = new Ssi(); $response = new Response('foo'); $response->setStatusCode(404); @@ -189,7 +190,7 @@ public function testHandleWhenResponseIsNot200AndAltIsPresent() protected function getCache($request, $response) { - $cache = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpCache\HttpCache')->setMethods(['getRequest', 'handle'])->disableOriginalConstructor()->getMock(); + $cache = $this->getMockBuilder(HttpCache::class)->setMethods(['getRequest', 'handle'])->disableOriginalConstructor()->getMock(); $cache->expects($this->any()) ->method('getRequest') ->willReturn($request) diff --git a/Tests/HttpCache/StoreTest.php b/Tests/HttpCache/StoreTest.php index 1f5f472802..239361bc8c 100644 --- a/Tests/HttpCache/StoreTest.php +++ b/Tests/HttpCache/StoreTest.php @@ -12,8 +12,10 @@ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; class StoreTest extends TestCase @@ -92,7 +94,7 @@ public function testSetsTheXContentDigestResponseHeaderBeforeStoring() { $cacheKey = $this->storeSimpleEntry(); $entries = $this->getStoreMetadata($cacheKey); - list(, $res) = $entries[0]; + [, $res] = $entries[0]; $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $res['x-content-digest'][0]); } @@ -103,7 +105,7 @@ public function testDoesNotTrustXContentDigestFromUpstream() $cacheKey = $this->store->write($this->request, $response); $entries = $this->getStoreMetadata($cacheKey); - list(, $res) = $entries[0]; + [, $res] = $entries[0]; $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $res['x-content-digest'][0]); $this->assertEquals('en9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', $response->headers->get('X-Content-Digest')); @@ -157,7 +159,7 @@ public function testFindsAStoredEntryWithLookup() $response = $this->store->lookup($this->request); $this->assertNotNull($response); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); + $this->assertInstanceOf(Response::class, $response); } public function testDoesNotFindAnEntryWithLookupWhenNoneExists() @@ -206,7 +208,7 @@ public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate() $this->storeSimpleEntry(); $this->store->invalidate($this->request); $response = $this->store->lookup($this->request); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response); + $this->assertInstanceOf(Response::class, $response); $this->assertFalse($response->isFresh()); } @@ -317,6 +319,17 @@ public function testPurgeHttpAndHttps() $this->assertEmpty($this->getStoreMetadata($requestHttps)); } + public function testDoesNotStorePrivateHeaders() + { + $request = Request::create('https://example.com/foo'); + $response = new Response('foo'); + $response->headers->setCookie(Cookie::fromString('foo=bar')); + + $this->store->write($request, $response); + $this->assertArrayNotHasKey('set-cookie', $this->getStoreMetadata($request)[0][1]); + $this->assertNotEmpty($response->headers->getCookies()); + } + protected function storeSimpleEntry($path = null, $headers = []) { if (null === $path) { diff --git a/Tests/HttpCache/SubRequestHandlerTest.php b/Tests/HttpCache/SubRequestHandlerTest.php index ea2d2b7a12..f17abb2028 100644 --- a/Tests/HttpCache/SubRequestHandlerTest.php +++ b/Tests/HttpCache/SubRequestHandlerTest.php @@ -42,6 +42,7 @@ public function testTrustedHeadersAreKept() $request->headers->set('X-Forwarded-Host', 'Good'); $request->headers->set('X-Forwarded-Port', '1234'); $request->headers->set('X-Forwarded-Proto', 'https'); + $request->headers->set('X-Forwarded-Prefix', '/admin'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); @@ -49,9 +50,10 @@ public function testTrustedHeadersAreKept() $this->assertSame('Good', $request->headers->get('X-Forwarded-Host')); $this->assertSame('1234', $request->headers->get('X-Forwarded-Port')); $this->assertSame('https', $request->headers->get('X-Forwarded-Proto')); + $this->assertSame('/admin', $request->headers->get('X-Forwarded-Prefix')); }); - SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true); + SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame($globalState, $this->getGlobalState()); } @@ -64,6 +66,7 @@ public function testUntrustedHeadersAreRemoved() $request->headers->set('X-Forwarded-Host', 'Evil'); $request->headers->set('X-Forwarded-Port', '1234'); $request->headers->set('X-Forwarded-Proto', 'http'); + $request->headers->set('X-Forwarded-Prefix', '/admin'); $request->headers->set('Forwarded', 'Evil2'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { @@ -72,10 +75,11 @@ public function testUntrustedHeadersAreRemoved() $this->assertFalse($request->headers->has('X-Forwarded-Host')); $this->assertFalse($request->headers->has('X-Forwarded-Port')); $this->assertFalse($request->headers->has('X-Forwarded-Proto')); + $this->assertFalse($request->headers->has('X-Forwarded-Prefix')); $this->assertSame('for="10.0.0.1";host="localhost";proto=http', $request->headers->get('Forwarded')); }); - SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true); + SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame(self::$globalState, $this->getGlobalState()); } @@ -97,7 +101,7 @@ public function testTrustedForwardedHeader() $this->assertSame(1234, $request->getPort()); }); - SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true); + SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame($globalState, $this->getGlobalState()); } @@ -112,15 +116,17 @@ public function testTrustedXForwardedForHeader() $request->headers->set('X-Forwarded-For', '10.0.0.2'); $request->headers->set('X-Forwarded-Host', 'foo.bar'); $request->headers->set('X-Forwarded-Proto', 'https'); + $request->headers->set('X-Forwarded-Prefix', '/admin'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); $this->assertSame('10.0.0.2', $request->getClientIp()); $this->assertSame('foo.bar', $request->getHttpHost()); $this->assertSame('https', $request->getScheme()); + $this->assertSame('/admin', $request->getBaseUrl()); }); - SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true); + SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame($globalState, $this->getGlobalState()); } @@ -143,7 +149,7 @@ public function __construct(\Closure $assertCallback) $this->assertCallback = $assertCallback; } - public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): Response + public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { $assertCallback = $this->assertCallback; $assertCallback($request, $type, $catch); diff --git a/Tests/HttpCache/TestHttpKernel.php b/Tests/HttpCache/TestHttpKernel.php index 933ed9b27c..471212f5e3 100644 --- a/Tests/HttpCache/TestHttpKernel.php +++ b/Tests/HttpCache/TestHttpKernel.php @@ -43,18 +43,18 @@ public function assert(\Closure $callback) { $trustedConfig = [Request::getTrustedProxies(), Request::getTrustedHeaderSet()]; - list($trustedProxies, $trustedHeaderSet, $backendRequest) = $this->backendRequest; + [$trustedProxies, $trustedHeaderSet, $backendRequest] = $this->backendRequest; Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); try { $callback($backendRequest); } finally { - list($trustedProxies, $trustedHeaderSet) = $trustedConfig; + [$trustedProxies, $trustedHeaderSet] = $trustedConfig; Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); } } - public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = false): Response + public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = false): Response { $this->catch = $catch; $this->backendRequest = [Request::getTrustedProxies(), Request::getTrustedHeaderSet(), $request]; diff --git a/Tests/HttpCache/TestMultipleHttpKernel.php b/Tests/HttpCache/TestMultipleHttpKernel.php index cef672e1f8..a8436d4b45 100644 --- a/Tests/HttpCache/TestMultipleHttpKernel.php +++ b/Tests/HttpCache/TestMultipleHttpKernel.php @@ -43,7 +43,7 @@ public function getBackendRequest() return $this->backendRequest; } - public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = false): Response + public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = false): Response { $this->backendRequest = $request; diff --git a/Tests/HttpClientKernelTest.php b/Tests/HttpClientKernelTest.php new file mode 100644 index 0000000000..27b3a82e03 --- /dev/null +++ b/Tests/HttpClientKernelTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\HttpClientKernel; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class HttpClientKernelTest extends TestCase +{ + public function testHandlePassesMaxRedirectsHttpClientOption() + { + $request = new Request(); + $request->attributes->set('http_client_options', ['max_redirects' => 50]); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->once())->method('getStatusCode')->willReturn(200); + + $client = $this->createMock(HttpClientInterface::class); + $client + ->expects($this->once()) + ->method('request') + ->willReturnCallback(function (string $method, string $uri, array $options) use ($request, $response) { + $this->assertSame($request->getMethod(), $method); + $this->assertSame($request->getUri(), $uri); + $this->assertArrayHasKey('max_redirects', $options); + $this->assertSame(50, $options['max_redirects']); + + return $response; + }); + + $kernel = new HttpClientKernel($client); + $kernel->handle($request); + } +} diff --git a/Tests/HttpKernelBrowserTest.php b/Tests/HttpKernelBrowserTest.php index fdcfc26667..b6e391625c 100644 --- a/Tests/HttpKernelBrowserTest.php +++ b/Tests/HttpKernelBrowserTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\HttpKernelBrowser; @@ -30,10 +31,10 @@ public function testDoRequest() $client->request('GET', '/'); $this->assertEquals('Request: /', $client->getResponse()->getContent(), '->doRequest() uses the request handler to make the request'); - $this->assertInstanceOf('Symfony\Component\BrowserKit\Request', $client->getInternalRequest()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\Request', $client->getRequest()); - $this->assertInstanceOf('Symfony\Component\BrowserKit\Response', $client->getInternalResponse()); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $client->getResponse()); + $this->assertInstanceOf(\Symfony\Component\BrowserKit\Request::class, $client->getInternalRequest()); + $this->assertInstanceOf(Request::class, $client->getRequest()); + $this->assertInstanceOf(\Symfony\Component\BrowserKit\Response::class, $client->getInternalResponse()); + $this->assertInstanceOf(Response::class, $client->getResponse()); $client->request('GET', 'http://www.example.com/'); $this->assertEquals('Request: /', $client->getResponse()->getContent(), '->doRequest() uses the request handler to make the request'); @@ -100,8 +101,8 @@ public function testUploadedFile() $client = new HttpKernelBrowser($kernel); $files = [ - ['tmp_name' => $source, 'name' => 'original', 'type' => 'mime/original', 'size' => null, 'error' => UPLOAD_ERR_OK], - new UploadedFile($source, 'original', 'mime/original', UPLOAD_ERR_OK, true), + ['tmp_name' => $source, 'name' => 'original', 'type' => 'mime/original', 'size' => null, 'error' => \UPLOAD_ERR_OK], + new UploadedFile($source, 'original', 'mime/original', \UPLOAD_ERR_OK, true), ]; $file = null; @@ -130,7 +131,7 @@ public function testUploadedFileWhenNoFileSelected() $kernel = new TestHttpKernel(); $client = new HttpKernelBrowser($kernel); - $file = ['tmp_name' => '', 'name' => '', 'type' => '', 'size' => 0, 'error' => UPLOAD_ERR_NO_FILE]; + $file = ['tmp_name' => '', 'name' => '', 'type' => '', 'size' => 0, 'error' => \UPLOAD_ERR_NO_FILE]; $client->request('POST', '/', [], ['foo' => $file]); @@ -142,25 +143,29 @@ public function testUploadedFileWhenNoFileSelected() public function testUploadedFileWhenSizeExceedsUploadMaxFileSize() { + if (UploadedFile::getMaxFilesize() > \PHP_INT_MAX) { + $this->markTestSkipped('Requires PHP_INT_MAX to be greater than "upload_max_filesize" and "post_max_size" ini settings'); + } + $source = tempnam(sys_get_temp_dir(), 'source'); $kernel = new TestHttpKernel(); $client = new HttpKernelBrowser($kernel); $file = $this - ->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile') - ->setConstructorArgs([$source, 'original', 'mime/original', UPLOAD_ERR_OK, true]) + ->getMockBuilder(UploadedFile::class) + ->setConstructorArgs([$source, 'original', 'mime/original', \UPLOAD_ERR_OK, true]) ->setMethods(['getSize', 'getClientSize']) ->getMock() ; /* should be modified when the getClientSize will be removed */ $file->expects($this->any()) ->method('getSize') - ->willReturn(INF) + ->willReturn(\PHP_INT_MAX) ; $file->expects($this->any()) ->method('getClientSize') - ->willReturn(PHP_INT_MAX) + ->willReturn(\PHP_INT_MAX) ; $client->request('POST', '/', [], [$file]); @@ -172,11 +177,22 @@ public function testUploadedFileWhenSizeExceedsUploadMaxFileSize() $file = $files[0]; $this->assertFalse($file->isValid()); - $this->assertEquals(UPLOAD_ERR_INI_SIZE, $file->getError()); + $this->assertEquals(\UPLOAD_ERR_INI_SIZE, $file->getError()); $this->assertEquals('mime/original', $file->getClientMimeType()); $this->assertEquals('original', $file->getClientOriginalName()); $this->assertEquals(0, $file->getSize()); unlink($source); } + + public function testAcceptHeaderNotSet() + { + $client = new HttpKernelBrowser(new TestHttpKernel()); + + $client->request('GET', '/'); + $this->assertFalse($client->getRequest()->headers->has('Accept')); + + $client->request('GET', '/', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); + $this->assertSame('application/ld+json', $client->getRequest()->headers->get('Accept')); + } } diff --git a/Tests/HttpKernelTest.php b/Tests/HttpKernelTest.php index b1d225b84e..38a09ef2d2 100644 --- a/Tests/HttpKernelTest.php +++ b/Tests/HttpKernelTest.php @@ -22,8 +22,10 @@ use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\ControllerDoesNotReturnResponseException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -32,18 +34,57 @@ class HttpKernelTest extends TestCase { public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrue() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }); - $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); + } + + public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsTrue() + { + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }, $requestStack); + + try { + $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + } catch (\Throwable $exception) { + } + + self::assertNull($requestStack->getCurrentRequest()); + } + + public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsFalse() + { + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }, $requestStack); + + try { + $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, false); + } catch (\Throwable $exception) { + } + + self::assertNull($requestStack->getCurrentRequest()); + } + + public function testRequestStackIsNotBrokenWhenControllerThrowsAnThrowable() + { + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \Error(); }, $requestStack); + + try { + $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + } catch (\Throwable $exception) { + } + + self::assertNull($requestStack->getCurrentRequest()); } public function testHandleWhenControllerThrowsAnExceptionAndCatchIsFalseAndNoListenerIsRegistered() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }); - $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, false); + $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); } public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithAHandlingListener() @@ -54,7 +95,7 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithAHand }); $kernel = $this->getHttpKernel($dispatcher, function () { throw new \RuntimeException('foo'); }); - $response = $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + $response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); $this->assertEquals('500', $response->getStatusCode()); $this->assertEquals('foo', $response->getContent()); @@ -72,7 +113,7 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithANonH $kernel = $this->getHttpKernel($dispatcher, function () use ($exception) { throw $exception; }); try { - $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); $this->fail('LogicException expected'); } catch (\RuntimeException $e) { $this->assertSame($exception, $e); @@ -157,7 +198,7 @@ public function testHandleWhenAListenerReturnsAResponse() public function testHandleWhenNoControllerIsFound() { - $this->expectException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException'); + $this->expectException(NotFoundHttpException::class); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, false); @@ -300,23 +341,39 @@ public function testTerminate() $this->assertEquals($response, $capturedResponse); } + public function testTerminateWithException() + { + $dispatcher = new EventDispatcher(); + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel($dispatcher, null, $requestStack); + + $dispatcher->addListener(KernelEvents::EXCEPTION, function (ExceptionEvent $event) use (&$capturedRequest, $requestStack) { + $capturedRequest = $requestStack->getCurrentRequest(); + $event->setResponse(new Response()); + }); + + $kernel->terminateWithException(new \Exception('boo'), $request = Request::create('/')); + $this->assertSame($request, $capturedRequest); + $this->assertNull($requestStack->getCurrentRequest()); + } + public function testVerifyRequestStackPushPopDuringHandle() { $request = new Request(); - $stack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->setMethods(['push', 'pop'])->getMock(); + $stack = $this->getMockBuilder(RequestStack::class)->setMethods(['push', 'pop'])->getMock(); $stack->expects($this->once())->method('push')->with($this->equalTo($request)); $stack->expects($this->once())->method('pop'); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, null, $stack); - $kernel->handle($request, HttpKernelInterface::MASTER_REQUEST); + $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST); } - public function testInconsistentClientIpsOnMasterRequests() + public function testInconsistentClientIpsOnMainRequests() { - $this->expectException('Symfony\Component\HttpKernel\Exception\BadRequestHttpException'); + $this->expectException(BadRequestHttpException::class); $request = new Request(); $request->setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_FORWARDED); $request->server->set('REMOTE_ADDR', '1.1.1.1'); @@ -329,7 +386,7 @@ public function testInconsistentClientIpsOnMasterRequests() }); $kernel = $this->getHttpKernel($dispatcher); - $kernel->handle($request, $kernel::MASTER_REQUEST, false); + $kernel->handle($request, $kernel::MAIN_REQUEST, false); Request::setTrustedProxies([], -1); } @@ -340,13 +397,13 @@ private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $contr $controller = function () { return new Response('Hello'); }; } - $controllerResolver = $this->getMockBuilder(ControllerResolverInterface::class)->getMock(); + $controllerResolver = $this->createMock(ControllerResolverInterface::class); $controllerResolver ->expects($this->any()) ->method('getController') ->willReturn($controller); - $argumentResolver = $this->getMockBuilder(ArgumentResolverInterface::class)->getMock(); + $argumentResolver = $this->createMock(ArgumentResolverInterface::class); $argumentResolver ->expects($this->any()) ->method('getArguments') diff --git a/Tests/KernelTest.php b/Tests/KernelTest.php index ece28e00c8..2dab2ec01d 100644 --- a/Tests/KernelTest.php +++ b/Tests/KernelTest.php @@ -16,13 +16,17 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; +use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Tests\Fixtures\KernelForTest; @@ -33,8 +37,10 @@ class KernelTest extends TestCase { protected function tearDown(): void { - $fs = new Filesystem(); - $fs->remove(__DIR__.'/Fixtures/var'); + try { + (new Filesystem())->remove(__DIR__.'/Fixtures/var'); + } catch (IOException $e) { + } } public function testConstructor() @@ -49,6 +55,14 @@ public function testConstructor() $this->assertLessThanOrEqual(microtime(true), $kernel->getStartTime()); } + public function testEmptyEnv() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Invalid environment provided to "%s": the environment cannot be empty.', KernelForTest::class)); + + new KernelForTest('', false); + } + public function testClone() { $env = 'test_env'; @@ -65,12 +79,12 @@ public function testClone() public function testClassNameValidityGetter() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The environment "test.env" contains invalid characters, it can only contain characters allowed in PHP class names.'); // We check the classname that will be generated by using a $env that // contains invalid characters. $env = 'test.env'; - $kernel = new KernelForTest($env, false); + $kernel = new KernelForTest($env, false, false); $kernel->boot(); } @@ -102,22 +116,20 @@ public function testInitializeContainerClearsOldContainers() public function testBootInitializesBundlesAndContainer() { - $kernel = $this->getKernel(['initializeBundles', 'initializeContainer']); + $kernel = $this->getKernel(['initializeBundles']); $kernel->expects($this->once()) ->method('initializeBundles'); - $kernel->expects($this->once()) - ->method('initializeContainer'); $kernel->boot(); } public function testBootSetsTheContainerToTheBundles() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle')->getMock(); + $bundle = $this->createMock(Bundle::class); $bundle->expects($this->once()) ->method('setContainer'); - $kernel = $this->getKernel(['initializeBundles', 'initializeContainer', 'getBundles']); + $kernel = $this->getKernel(['initializeBundles', 'getBundles']); $kernel->expects($this->once()) ->method('getBundles') ->willReturn([$bundle]); @@ -128,7 +140,7 @@ public function testBootSetsTheContainerToTheBundles() public function testBootSetsTheBootedFlagToTrue() { // use test kernel to access isBooted() - $kernel = $this->getKernel(['initializeBundles', 'initializeContainer']); + $kernel = $this->getKernel(['initializeBundles']); $kernel->boot(); $this->assertTrue($kernel->isBooted()); @@ -136,7 +148,7 @@ public function testBootSetsTheBootedFlagToTrue() public function testClassCacheIsNotLoadedByDefault() { - $kernel = $this->getKernel(['initializeBundles', 'initializeContainer', 'doLoadClassCache']); + $kernel = $this->getKernel(['initializeBundles', 'doLoadClassCache']); $kernel->expects($this->never()) ->method('doLoadClassCache'); @@ -145,7 +157,7 @@ public function testClassCacheIsNotLoadedByDefault() public function testBootKernelSeveralTimesOnlyInitializesBundlesOnce() { - $kernel = $this->getKernel(['initializeBundles', 'initializeContainer']); + $kernel = $this->getKernel(['initializeBundles']); $kernel->expects($this->once()) ->method('initializeBundles'); @@ -155,7 +167,7 @@ public function testBootKernelSeveralTimesOnlyInitializesBundlesOnce() public function testShutdownCallsShutdownOnAllBundles() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle')->getMock(); + $bundle = $this->createMock(Bundle::class); $bundle->expects($this->once()) ->method('shutdown'); @@ -167,7 +179,7 @@ public function testShutdownCallsShutdownOnAllBundles() public function testShutdownGivesNullContainerToAllBundles() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle')->getMock(); + $bundle = $this->createMock(Bundle::class); $bundle->expects($this->exactly(2)) ->method('setContainer') ->withConsecutive( @@ -186,11 +198,11 @@ public function testShutdownGivesNullContainerToAllBundles() public function testHandleCallsHandleOnHttpKernel() { - $type = HttpKernelInterface::MASTER_REQUEST; + $type = HttpKernelInterface::MAIN_REQUEST; $catch = true; $request = new Request(); - $httpKernelMock = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernel') + $httpKernelMock = $this->getMockBuilder(HttpKernel::class) ->disableOriginalConstructor() ->getMock(); $httpKernelMock @@ -208,11 +220,11 @@ public function testHandleCallsHandleOnHttpKernel() public function testHandleBootsTheKernel() { - $type = HttpKernelInterface::MASTER_REQUEST; + $type = HttpKernelInterface::MAIN_REQUEST; $catch = true; $request = new Request(); - $httpKernelMock = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernel') + $httpKernelMock = $this->getMockBuilder(HttpKernel::class) ->disableOriginalConstructor() ->getMock(); @@ -227,10 +239,37 @@ public function testHandleBootsTheKernel() $kernel->handle($request, $type, $catch); } - public function testStripComments() + /** + * @dataProvider getStripCommentsCodes + */ + public function testStripComments(string $source, string $expected) { - $source = <<<'EOF' + $output = Kernel::stripComments($source); + + // Heredocs are preserved, making the output mixing Unix and Windows line + // endings, switching to "\n" everywhere on Windows to avoid failure. + if ('\\' === \DIRECTORY_SEPARATOR) { + $expected = str_replace("\r\n", "\n", $expected); + $output = str_replace("\r\n", "\n", $output); + } + + $this->assertEquals($expected, $output); + } + + public function getStripCommentsCodes(): array + { + return [ + ['assertEquals($expected, $output); +EOF + ], + ]; } public function testSerialize() @@ -320,25 +351,25 @@ public function testSerialize() public function testLocateResourceThrowsExceptionWhenNameIsNotValid() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->getKernel()->locateResource('Foo'); } public function testLocateResourceThrowsExceptionWhenNameIsUnsafe() { - $this->expectException('RuntimeException'); + $this->expectException(\RuntimeException::class); $this->getKernel()->locateResource('@FooBundle/../bar'); } public function testLocateResourceThrowsExceptionWhenBundleDoesNotExist() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $this->getKernel()->locateResource('@FooBundle/config/routing.xml'); } public function testLocateResourceThrowsExceptionWhenResourceDoesNotExist() { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $kernel = $this->getKernel(['getBundle']); $kernel ->expects($this->once()) @@ -382,7 +413,7 @@ public function testLocateResourceOnDirectories() public function testInitializeBundleThrowsExceptionWhenRegisteringTwoBundlesWithTheSameName() { - $this->expectException('LogicException'); + $this->expectException(\LogicException::class); $this->expectExceptionMessage('Trying to register two bundles with the same name "DuplicateName"'); $fooBundle = $this->getBundle(__DIR__.'/Fixtures/FooBundle', null, 'FooBundle', 'DuplicateName'); $barBundle = $this->getBundle(__DIR__.'/Fixtures/BarBundle', null, 'BarBundle', 'DuplicateName'); @@ -416,7 +447,7 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface() $this->assertFalse($httpKernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface'); // implements TerminableInterface - $httpKernelMock = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernel') + $httpKernelMock = $this->getMockBuilder(HttpKernel::class) ->disableOriginalConstructor() ->setMethods(['terminate']) ->getMock(); @@ -453,7 +484,7 @@ public function testProjectDirExtension() public function testKernelReset() { - (new Filesystem())->remove(__DIR__.'/Fixtures/var/cache'); + $this->tearDown(); $kernel = new CustomProjectDirKernel(); $kernel->boot(); @@ -477,6 +508,37 @@ public function testKernelReset() $this->assertFileExists(\dirname($containerFile).'.legacy'); } + public function testKernelExtension() + { + $kernel = new class() extends CustomProjectDirKernel implements ExtensionInterface { + public function load(array $configs, ContainerBuilder $container) + { + $container->setParameter('test.extension-registered', true); + } + + public function getNamespace(): string + { + return ''; + } + + /** + * @return string|false + */ + public function getXsdValidationBasePath() + { + return false; + } + + public function getAlias(): string + { + return 'test-extension'; + } + }; + $kernel->boot(); + + $this->assertTrue($kernel->getContainer()->getParameter('test.extension-registered')); + } + public function testKernelPass() { $kernel = new PassKernel(); @@ -534,13 +596,13 @@ public function testKernelStartTimeIsResetWhileBootingAlreadyBootedKernel() $kernel->boot(); $preReBoot = $kernel->getStartTime(); - sleep(3600); //Intentionally large value to detect if ClockMock ever breaks + sleep(3600); // Intentionally large value to detect if ClockMock ever breaks $kernel->reboot(null); $this->assertGreaterThan($preReBoot, $kernel->getStartTime()); } - public function testAnonymousKernelGeneratesValidContainerClass(): void + public function testAnonymousKernelGeneratesValidContainerClass() { $kernel = new class('test', true) extends Kernel { public function registerBundles(): iterable @@ -567,7 +629,7 @@ public function getContainerClass(): string protected function getBundle($dir = null, $parent = null, $className = null, $bundleName = null): BundleInterface { $bundle = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Bundle\BundleInterface') + ->getMockBuilder(BundleInterface::class) ->setMethods(['getPath', 'getName']) ->disableOriginalConstructor() ; @@ -581,7 +643,7 @@ protected function getBundle($dir = null, $parent = null, $className = null, $bu $bundle ->expects($this->any()) ->method('getName') - ->willReturn(null === $bundleName ? \get_class($bundle) : $bundleName) + ->willReturn($bundleName ?? \get_class($bundle)) ; $bundle @@ -627,7 +689,7 @@ public function terminate() $this->terminateCalled = true; } - public function handle(Request $request, int $type = self::MASTER_REQUEST, bool $catch = true): Response + public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response { } diff --git a/Tests/Log/LoggerTest.php b/Tests/Log/LoggerTest.php index 5c7da41957..5996e6e72a 100644 --- a/Tests/Log/LoggerTest.php +++ b/Tests/Log/LoggerTest.php @@ -48,7 +48,7 @@ protected function tearDown(): void public static function assertLogsMatch(array $expected, array $given) { foreach ($given as $k => $line) { - self::assertThat(1 === preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[\+-][0-9]{2}:[0-9]{2} '.preg_quote($expected[$k]).'/', $line), self::isTrue(), "\"$line\" do not match expected pattern \"$expected[$k]\""); + self::assertSame(1, preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[\+-][0-9]{2}:[0-9]{2} '.preg_quote($expected[$k]).'/', $line), "\"$line\" do not match expected pattern \"$expected[$k]\""); } } @@ -59,7 +59,7 @@ public static function assertLogsMatch(array $expected, array $given) */ public function getLogs(): array { - return file($this->tmpFile, FILE_IGNORE_NEW_LINES); + return file($this->tmpFile, \FILE_IGNORE_NEW_LINES); } public function testImplements() @@ -109,19 +109,19 @@ public function testLogLevelDisabled() public function testThrowsOnInvalidLevel() { - $this->expectException('Psr\Log\InvalidArgumentException'); + $this->expectException(\Psr\Log\InvalidArgumentException::class); $this->logger->log('invalid level', 'Foo'); } public function testThrowsOnInvalidMinLevel() { - $this->expectException('Psr\Log\InvalidArgumentException'); + $this->expectException(\Psr\Log\InvalidArgumentException::class); new Logger('invalid'); } public function testInvalidOutput() { - $this->expectException('Psr\Log\InvalidArgumentException'); + $this->expectException(\Psr\Log\InvalidArgumentException::class); new Logger(LogLevel::DEBUG, '/'); } @@ -186,7 +186,7 @@ public function testContextExceptionKeyCanBeExceptionOrOtherValues() public function testFormatter() { $this->logger = new Logger(LogLevel::DEBUG, $this->tmpFile, function ($level, $message, $context) { - return json_encode(['level' => $level, 'message' => $message, 'context' => $context]).PHP_EOL; + return json_encode(['level' => $level, 'message' => $message, 'context' => $context]); }); $this->logger->error('An error', ['foo' => 'bar']); @@ -196,6 +196,26 @@ public function testFormatter() '{"level":"warning","message":"A warning","context":{"baz":"bar"}}', ], $this->getLogs()); } + + public function testLogsWithoutOutput() + { + $oldErrorLog = ini_set('error_log', $this->tmpFile); + + $logger = new Logger(); + $logger->error('test'); + $logger->critical('test'); + + $expected = [ + '[error] test', + '[critical] test', + ]; + + foreach ($this->getLogs() as $k => $line) { + $this->assertSame(1, preg_match('/\[[\w\/\-: ]+\] '.preg_quote($expected[$k]).'/', $line), "\"$line\" do not match expected pattern \"$expected[$k]\""); + } + + ini_set('error_log', $oldErrorLog); + } } class DummyTest diff --git a/Tests/Profiler/FileProfilerStorageTest.php b/Tests/Profiler/FileProfilerStorageTest.php index 2df9a94884..8aede3181c 100644 --- a/Tests/Profiler/FileProfilerStorageTest.php +++ b/Tests/Profiler/FileProfilerStorageTest.php @@ -213,26 +213,25 @@ public function testRetrieveByUrl() public function testStoreTime() { - $dt = new \DateTime('now'); - $start = $dt->getTimestamp(); + $start = $now = time(); for ($i = 0; $i < 3; ++$i) { - $dt->modify('+1 minute'); + $now += 60; $profile = new Profile('time_'.$i); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/'); - $profile->setTime($dt->getTimestamp()); + $profile->setTime($now); $profile->setMethod('GET'); $this->storage->write($profile); } - $records = $this->storage->find('', '', 3, 'GET', $start, time() + 3 * 60); + $records = $this->storage->find('', '', 3, 'GET', $start, $start + 3 * 60); $this->assertCount(3, $records, '->find() returns all previously added records'); $this->assertEquals('time_2', $records[0]['token'], '->find() returns records ordered by time in descendant order'); $this->assertEquals('time_1', $records[1]['token'], '->find() returns records ordered by time in descendant order'); $this->assertEquals('time_0', $records[2]['token'], '->find() returns records ordered by time in descendant order'); - $records = $this->storage->find('', '', 3, 'GET', $start, time() + 2 * 60); + $records = $this->storage->find('', '', 3, 'GET', $start, $start + 2 * 60); $this->assertCount(2, $records, '->find() should return only first two of the previously added records'); } @@ -298,7 +297,7 @@ public function testDuplicates() $profile->setUrl('http://example.net/'); $profile->setMethod('GET'); - ///three duplicates + // three duplicates $this->storage->write($profile); $this->storage->write($profile); $this->storage->write($profile); @@ -354,7 +353,7 @@ public function testReadLineFromFile() $h = tmpfile(); fwrite($h, "line1\n\n\nline2\n"); - fseek($h, 0, SEEK_END); + fseek($h, 0, \SEEK_END); $this->assertEquals('line2', $r->invoke($this->storage, $h)); $this->assertEquals('line1', $r->invoke($this->storage, $h)); diff --git a/UriSigner.php b/UriSigner.php index df08dd69fc..38931ce170 100644 --- a/UriSigner.php +++ b/UriSigner.php @@ -39,7 +39,7 @@ public function __construct(string $secret, string $parameter = '_hash') * The given URI is signed by adding the query string parameter * which value depends on the URI and the secret. * - * @return string The signed URI + * @return string */ public function sign(string $uri) { @@ -59,7 +59,7 @@ public function sign(string $uri) /** * Checks that a URI contains the correct hash. * - * @return bool True if the URI is signed correctly, false otherwise + * @return bool */ public function check(string $uri) { @@ -95,16 +95,16 @@ private function computeHash(string $uri): string private function buildUrl(array $url, array $params = []): string { - ksort($params, SORT_STRING); + ksort($params, \SORT_STRING); $url['query'] = http_build_query($params, '', '&'); $scheme = isset($url['scheme']) ? $url['scheme'].'://' : ''; - $host = isset($url['host']) ? $url['host'] : ''; + $host = $url['host'] ?? ''; $port = isset($url['port']) ? ':'.$url['port'] : ''; - $user = isset($url['user']) ? $url['user'] : ''; + $user = $url['user'] ?? ''; $pass = isset($url['pass']) ? ':'.$url['pass'] : ''; $pass = ($user || $pass) ? "$pass@" : ''; - $path = isset($url['path']) ? $url['path'] : ''; + $path = $url['path'] ?? ''; $query = isset($url['query']) && $url['query'] ? '?'.$url['query'] : ''; $fragment = isset($url['fragment']) ? '#'.$url['fragment'] : ''; diff --git a/composer.json b/composer.json index c016af2369..09682db49d 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/http-kernel", "type": "library", - "description": "Symfony HttpKernel Component", + "description": "Provides a structured process for converting a Request into a Response", "keywords": [], "homepage": "https://symfony.com/", "license": "MIT", @@ -17,42 +17,43 @@ ], "require": { "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", - "symfony/error-handler": "^4.4|^5.0", - "symfony/event-dispatcher": "^5.0", - "symfony/http-foundation": "^4.4|^5.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^5.0|^6.0", + "symfony/http-foundation": "^5.3.7|^6.0", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.15", - "psr/log": "~1.0" + "symfony/polyfill-php80": "^1.16", + "psr/log": "^1|^2" }, "require-dev": { - "symfony/browser-kit": "^4.4|^5.0", - "symfony/config": "^5.0", - "symfony/console": "^4.4|^5.0", - "symfony/css-selector": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/dom-crawler": "^4.4|^5.0", - "symfony/expression-language": "^4.4|^5.0", - "symfony/finder": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/routing": "^4.4|^5.0", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/translation": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "psr/cache": "~1.0", - "twig/twig": "^2.4|^3.0" + "symfony/browser-kit": "^5.4|^6.0", + "symfony/config": "^5.0|^6.0", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/css-selector": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/dom-crawler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/http-client-contracts": "^1.1|^2|^3", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/routing": "^4.4|^5.0|^6.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0", + "symfony/translation": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2|^3", + "psr/cache": "^1.0|^2.0|^3.0", + "twig/twig": "^2.13|^3.0.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "conflict": { - "symfony/browser-kit": "<4.4", + "symfony/browser-kit": "<5.4", "symfony/cache": "<5.0", "symfony/config": "<5.0", "symfony/console": "<4.4", "symfony/form": "<5.0", - "symfony/dependency-injection": "<4.4", + "symfony/dependency-injection": "<5.3", "symfony/doctrine-bridge": "<5.0", "symfony/http-client": "<5.0", "symfony/mailer": "<5.0", @@ -60,7 +61,7 @@ "symfony/translation": "<5.0", "symfony/twig-bridge": "<5.0", "symfony/validator": "<5.0", - "twig/twig": "<2.4" + "twig/twig": "<2.13" }, "suggest": { "symfony/browser-kit": "", @@ -74,10 +75,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - } + "minimum-stability": "dev" }