diff --git a/.appveyor.yml b/.appveyor.yml index 889aafe26929b..fddaf7b75802c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,7 @@ build: false clone_depth: 2 clone_folder: c:\projects\symfony +image: Visual Studio 2019 cache: - composer.phar @@ -16,17 +17,17 @@ init: install: - mkdir c:\php && cd c:\php - - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php-7.1.3-Win32-VC14-x86.zip - - 7z x php-7.1.3-Win32-VC14-x86.zip -y >nul + - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php-8.0.2-Win32-vs16-x86.zip + - 7z x php-8.0.2-Win32-vs16-x86.zip -y >nul - cd ext - - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.18-7.1-ts-vc14-x86.zip - - 7z x php_apcu-5.1.18-7.1-ts-vc14-x86.zip -y >nul - - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_redis-5.1.1-7.1-ts-vc14-x86.zip - - 7z x php_redis-5.1.1-7.1-ts-vc14-x86.zip -y >nul + - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.21-8.0-ts-vs16-x86.zip + - 7z x php_apcu-5.1.21-8.0-ts-vs16-x86.zip -y >nul + - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_redis-5.3.5-8.0-ts-vs16-x86.zip + - 7z x php_redis-5.3.5-8.0-ts-vs16-x86.zip -y >nul - cd .. - copy /Y php.ini-development php.ini-min - echo memory_limit=-1 >> php.ini-min - - echo serialize_precision=14 >> php.ini-min + - echo serialize_precision=-1 >> php.ini-min - echo max_execution_time=1200 >> php.ini-min - echo post_max_size=4G >> php.ini-min - echo upload_max_filesize=4G >> php.ini-min @@ -45,6 +46,7 @@ install: - echo extension=php_fileinfo.dll >> php.ini-max - echo extension=php_pdo_sqlite.dll >> php.ini-max - echo extension=php_curl.dll >> php.ini-max + - echo extension=php_sodium.dll >> php.ini-max - copy /Y php.ini-max php.ini - cd c:\projects\symfony - IF NOT EXIST composer.phar (appveyor DownloadFile https://github.com/composer/composer/releases/download/2.0.0/composer.phar) @@ -64,7 +66,10 @@ test_script: - SET SYMFONY_PHPUNIT_SKIPPED_TESTS=phpunit.skipped - copy /Y c:\php\php.ini-min c:\php\php.ini - IF %APPVEYOR_REPO_BRANCH:~-2% neq .x (rm -Rf src\Symfony\Bridge\PhpUnit) + - mv src\Symfony\Component\HttpClient\phpunit.xml.dist src\Symfony\Component\HttpClient\phpunit.xml - php phpunit src\Symfony --exclude-group tty,benchmark,intl-data,network,transient-on-windows || SET X=!errorlevel! + - php phpunit src\Symfony\Component\HttpClient || SET X=!errorlevel! - copy /Y c:\php\php.ini-max c:\php\php.ini - php phpunit src\Symfony --exclude-group tty,benchmark,intl-data,network,transient-on-windows || SET X=!errorlevel! + - php phpunit src\Symfony\Component\HttpClient || SET X=!errorlevel! - exit %X% diff --git a/.gitattributes b/.gitattributes index c255f66722075..d30fb22a3bdbb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ /src/Symfony/Contracts export-ignore /src/Symfony/Bridge/PhpUnit export-ignore +/src/Symfony/Component/Mailer/Bridge export-ignore +/src/Symfony/Component/Messenger/Bridge export-ignore +/src/Symfony/Component/Notifier/Bridge export-ignore +/src/Symfony/Component/Runtime export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0870dcfdd5cc4..b958112d1b58a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,8 +31,12 @@ # Messenger /src/Symfony/Bridge/Doctrine/Messenger/ @sroze /src/Symfony/Component/Messenger/ @sroze +# Notifer +/src/Symfony/Component/Notifier/ @OskarStark # OptionsResolver /src/Symfony/Component/OptionsResolver/ @yceruto +# PasswordHasher +/src/Symfony/Component/PasswordHasher/ @chalasr # PropertyInfo /src/Symfony/Component/PropertyInfo/ @dunglas /src/Symfony/Bridge/Doctrine/PropertyInfo/ @dunglas diff --git a/.github/composer-config.json b/.github/composer-config.json index 6e17bc21e4582..65919964fa8a1 100644 --- a/.github/composer-config.json +++ b/.github/composer-config.json @@ -4,7 +4,10 @@ "preferred-install": { "symfony/form": "source", "symfony/http-kernel": "source", + "symfony/messenger": "source", + "symfony/notifier": "source", "symfony/proxy-manager-bridge": "source", + "symfony/translation": "source", "symfony/validator": "source", "*": "dist" } diff --git a/.github/expected-missing-return-types.diff b/.github/expected-missing-return-types.diff new file mode 100644 index 0000000000000..d45a91c61c7b8 --- /dev/null +++ b/.github/expected-missing-return-types.diff @@ -0,0 +1,1015 @@ +# Run these steps to update this file: +sed -i 's/ *"\*\*\/Tests\/"//' composer.json +composer u -o +SYMFONY_PATCH_TYPE_DECLARATIONS='force=2&php=8.0' php .github/patch-types.php +head=$(sed '/^diff /Q' .github/expected-missing-return-types.diff) +(echo "$head" && echo && git diff -U2 composer.json src/) > .github/expected-missing-return-types.diff + +diff --git a/composer.json b/composer.json +index 978743d34d..1f185f682c 100644 +--- a/composer.json ++++ b/composer.json +@@ -180,5 +180,5 @@ + ], + "exclude-from-classmap": [ +- "**/Tests/" ++ + ] + }, +diff --git a/src/Symfony/Component/BrowserKit/AbstractBrowser.php b/src/Symfony/Component/BrowserKit/AbstractBrowser.php +index 152050159b..e2ec1aeea2 100644 +--- a/src/Symfony/Component/BrowserKit/AbstractBrowser.php ++++ b/src/Symfony/Component/BrowserKit/AbstractBrowser.php +@@ -408,5 +408,5 @@ abstract class AbstractBrowser + * @throws \RuntimeException When processing returns exit code + */ +- protected function doRequestInProcess(object $request) ++ protected function doRequestInProcess(object $request): object + { + $deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec'); +@@ -441,5 +441,5 @@ abstract class AbstractBrowser + * @return object + */ +- abstract protected function doRequest(object $request); ++ abstract protected function doRequest(object $request): object; + + /** +@@ -460,5 +460,5 @@ abstract class AbstractBrowser + * @return object + */ +- protected function filterRequest(Request $request) ++ protected function filterRequest(Request $request): object + { + return $request; +@@ -470,5 +470,5 @@ abstract class AbstractBrowser + * @return Response + */ +- protected function filterResponse(object $response) ++ protected function filterResponse(object $response): Response + { + return $response; +diff --git a/src/Symfony/Component/Config/Definition/ConfigurationInterface.php b/src/Symfony/Component/Config/Definition/ConfigurationInterface.php +index 7b5d443fe6..d64ae0d024 100644 +--- a/src/Symfony/Component/Config/Definition/ConfigurationInterface.php ++++ b/src/Symfony/Component/Config/Definition/ConfigurationInterface.php +@@ -26,4 +26,4 @@ interface ConfigurationInterface + * @return TreeBuilder + */ +- public function getConfigTreeBuilder(); ++ public function getConfigTreeBuilder(): TreeBuilder; + } +diff --git a/src/Symfony/Component/Config/FileLocator.php b/src/Symfony/Component/Config/FileLocator.php +index 21122e52c9..206029e705 100644 +--- a/src/Symfony/Component/Config/FileLocator.php ++++ b/src/Symfony/Component/Config/FileLocator.php +@@ -34,5 +34,5 @@ class FileLocator implements FileLocatorInterface + * {@inheritdoc} + */ +- public function locate(string $name, string $currentPath = null, bool $first = true) ++ public function locate(string $name, string $currentPath = null, bool $first = true): string|array + { + if ('' === $name) { +diff --git a/src/Symfony/Component/Config/FileLocatorInterface.php b/src/Symfony/Component/Config/FileLocatorInterface.php +index e3ca1d49c4..526d350484 100644 +--- a/src/Symfony/Component/Config/FileLocatorInterface.php ++++ b/src/Symfony/Component/Config/FileLocatorInterface.php +@@ -31,4 +31,4 @@ interface FileLocatorInterface + * @throws FileLocatorFileNotFoundException If a file is not found + */ +- public function locate(string $name, string $currentPath = null, bool $first = true); ++ public function locate(string $name, string $currentPath = null, bool $first = true): string|array; + } +diff --git a/src/Symfony/Component/Config/Loader/FileLoader.php b/src/Symfony/Component/Config/Loader/FileLoader.php +index c479f75d34..0d16baaff7 100644 +--- a/src/Symfony/Component/Config/Loader/FileLoader.php ++++ b/src/Symfony/Component/Config/Loader/FileLoader.php +@@ -69,5 +69,5 @@ abstract class FileLoader extends Loader + * @throws FileLocatorFileNotFoundException + */ +- public function import(mixed $resource, string $type = null, bool $ignoreErrors = false, string $sourceResource = null, string|array $exclude = null) ++ public function import(mixed $resource, string $type = null, bool $ignoreErrors = false, string $sourceResource = null, string|array $exclude = null): mixed + { + if (\is_string($resource) && \strlen($resource) !== ($i = strcspn($resource, '*?{[')) && !str_contains($resource, "\n")) { +diff --git a/src/Symfony/Component/Config/Loader/Loader.php b/src/Symfony/Component/Config/Loader/Loader.php +index e0974fb151..f698b5271b 100644 +--- a/src/Symfony/Component/Config/Loader/Loader.php ++++ b/src/Symfony/Component/Config/Loader/Loader.php +@@ -50,5 +50,5 @@ abstract class Loader implements LoaderInterface + * @return mixed + */ +- public function import(mixed $resource, string $type = null) ++ public function import(mixed $resource, string $type = null): mixed + { + return $this->resolve($resource, $type)->load($resource, $type); +diff --git a/src/Symfony/Component/Config/Loader/LoaderInterface.php b/src/Symfony/Component/Config/Loader/LoaderInterface.php +index b94a4378f5..db502e12a7 100644 +--- a/src/Symfony/Component/Config/Loader/LoaderInterface.php ++++ b/src/Symfony/Component/Config/Loader/LoaderInterface.php +@@ -26,5 +26,5 @@ interface LoaderInterface + * @throws \Exception If something went wrong + */ +- public function load(mixed $resource, string $type = null); ++ public function load(mixed $resource, string $type = null): mixed; + + /** +@@ -35,5 +35,5 @@ interface LoaderInterface + * @return bool + */ +- public function supports(mixed $resource, string $type = null); ++ public function supports(mixed $resource, string $type = null): bool; + + /** +@@ -42,5 +42,5 @@ interface LoaderInterface + * @return LoaderResolverInterface + */ +- public function getResolver(); ++ public function getResolver(): LoaderResolverInterface; + + /** +diff --git a/src/Symfony/Component/Config/ResourceCheckerInterface.php b/src/Symfony/Component/Config/ResourceCheckerInterface.php +index 6b1c6c5fbe..bb80ed461e 100644 +--- a/src/Symfony/Component/Config/ResourceCheckerInterface.php ++++ b/src/Symfony/Component/Config/ResourceCheckerInterface.php +@@ -33,5 +33,5 @@ interface ResourceCheckerInterface + * @return bool + */ +- public function supports(ResourceInterface $metadata); ++ public function supports(ResourceInterface $metadata): bool; + + /** +@@ -42,4 +42,4 @@ interface ResourceCheckerInterface + * @return bool + */ +- public function isFresh(ResourceInterface $resource, int $timestamp); ++ public function isFresh(ResourceInterface $resource, int $timestamp): bool; + } +diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php +index 09234f5eb5..24d995ad6d 100644 +--- a/src/Symfony/Component/Console/Application.php ++++ b/src/Symfony/Component/Console/Application.php +@@ -218,5 +218,5 @@ class Application implements ResetInterface + * @return int 0 if everything went fine, or an error code + */ +- public function doRun(InputInterface $input, OutputInterface $output) ++ public function doRun(InputInterface $input, OutputInterface $output): int + { + if (true === $input->hasParameterOption(['--version', '-V'], true)) { +@@ -445,5 +445,5 @@ class Application implements ResetInterface + * @return string + */ +- public function getLongVersion() ++ public function getLongVersion(): string + { + if ('UNKNOWN' !== $this->getName()) { +@@ -488,5 +488,5 @@ class Application implements ResetInterface + * @return Command|null + */ +- public function add(Command $command) ++ public function add(Command $command): ?Command + { + $this->init(); +@@ -525,5 +525,5 @@ class Application implements ResetInterface + * @throws CommandNotFoundException When given command name does not exist + */ +- public function get(string $name) ++ public function get(string $name): Command + { + $this->init(); +@@ -632,5 +632,5 @@ class Application implements ResetInterface + * @throws CommandNotFoundException When command name is incorrect or ambiguous + */ +- public function find(string $name) ++ public function find(string $name): Command + { + $this->init(); +@@ -742,5 +742,5 @@ class Application implements ResetInterface + * @return Command[] + */ +- public function all(string $namespace = null) ++ public function all(string $namespace = null): array + { + $this->init(); +@@ -941,5 +941,5 @@ class Application implements ResetInterface + * @return int 0 if everything went fine, or an error code + */ +- protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) ++ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int + { + foreach ($command->getHelperSet() as $helper) { +diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php +index e69bae0982..3390628d0d 100644 +--- a/src/Symfony/Component/Console/Command/Command.php ++++ b/src/Symfony/Component/Console/Command/Command.php +@@ -171,5 +171,5 @@ class Command + * @return bool + */ +- public function isEnabled() ++ public function isEnabled(): bool + { + return true; +@@ -197,5 +197,5 @@ class Command + * @see setCode() + */ +- protected function execute(InputInterface $input, OutputInterface $output) ++ protected function execute(InputInterface $input, OutputInterface $output): int + { + throw new LogicException('You must override the execute() method in the concrete command class.'); +diff --git a/src/Symfony/Component/Console/Helper/HelperInterface.php b/src/Symfony/Component/Console/Helper/HelperInterface.php +index 1d2b7bfb84..cb1f66152d 100644 +--- a/src/Symfony/Component/Console/Helper/HelperInterface.php ++++ b/src/Symfony/Component/Console/Helper/HelperInterface.php +@@ -34,4 +34,4 @@ interface HelperInterface + * @return string + */ +- public function getName(); ++ public function getName(): string; + } +diff --git a/src/Symfony/Component/Console/Input/InputInterface.php b/src/Symfony/Component/Console/Input/InputInterface.php +index 024da1884e..943790e875 100644 +--- a/src/Symfony/Component/Console/Input/InputInterface.php ++++ b/src/Symfony/Component/Console/Input/InputInterface.php +@@ -54,5 +54,5 @@ interface InputInterface + * @return mixed + */ +- public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false); ++ public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed; + + /** +@@ -84,5 +84,5 @@ interface InputInterface + * @throws InvalidArgumentException When argument given doesn't exist + */ +- public function getArgument(string $name); ++ public function getArgument(string $name): mixed; + + /** +@@ -112,5 +112,5 @@ interface InputInterface + * @throws InvalidArgumentException When option given doesn't exist + */ +- public function getOption(string $name); ++ public function getOption(string $name): mixed; + + /** +diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +index e9fa5a6808..016e9d893a 100644 +--- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php ++++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +@@ -71,5 +71,5 @@ abstract class AbstractRecursivePass implements CompilerPassInterface + * @return mixed + */ +- protected function processValue(mixed $value, bool $isRoot = false) ++ protected function processValue(mixed $value, bool $isRoot = false): mixed + { + if (\is_array($value)) { +diff --git a/src/Symfony/Component/DependencyInjection/Container.php b/src/Symfony/Component/DependencyInjection/Container.php +index 0532120adf..78fba5ef79 100644 +--- a/src/Symfony/Component/DependencyInjection/Container.php ++++ b/src/Symfony/Component/DependencyInjection/Container.php +@@ -108,5 +108,5 @@ class Container implements ContainerInterface, ResetInterface + * @throws InvalidArgumentException if the parameter is not defined + */ +- public function getParameter(string $name) ++ public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null + { + return $this->parameterBag->get($name); +diff --git a/src/Symfony/Component/DependencyInjection/ContainerInterface.php b/src/Symfony/Component/DependencyInjection/ContainerInterface.php +index aa5d6b317e..31ffbca4ef 100644 +--- a/src/Symfony/Component/DependencyInjection/ContainerInterface.php ++++ b/src/Symfony/Component/DependencyInjection/ContainerInterface.php +@@ -53,5 +53,5 @@ interface ContainerInterface extends PsrContainerInterface + * @throws InvalidArgumentException if the parameter is not defined + */ +- public function getParameter(string $name); ++ public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null; + + public function hasParameter(string $name): bool; +diff --git a/src/Symfony/Component/DependencyInjection/Extension/ConfigurationExtensionInterface.php b/src/Symfony/Component/DependencyInjection/Extension/ConfigurationExtensionInterface.php +index a42967f4da..4e86e16f9d 100644 +--- a/src/Symfony/Component/DependencyInjection/Extension/ConfigurationExtensionInterface.php ++++ b/src/Symfony/Component/DependencyInjection/Extension/ConfigurationExtensionInterface.php +@@ -27,4 +27,4 @@ interface ConfigurationExtensionInterface + * @return ConfigurationInterface|null + */ +- public function getConfiguration(array $config, ContainerBuilder $container); ++ public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface; + } +diff --git a/src/Symfony/Component/DependencyInjection/Extension/Extension.php b/src/Symfony/Component/DependencyInjection/Extension/Extension.php +index d553203c43..1163f4b107 100644 +--- a/src/Symfony/Component/DependencyInjection/Extension/Extension.php ++++ b/src/Symfony/Component/DependencyInjection/Extension/Extension.php +@@ -32,5 +32,5 @@ abstract class Extension implements ExtensionInterface, ConfigurationExtensionIn + * {@inheritdoc} + */ +- public function getXsdValidationBasePath() ++ public function getXsdValidationBasePath(): string|false + { + return false; +@@ -40,5 +40,5 @@ abstract class Extension implements ExtensionInterface, ConfigurationExtensionIn + * {@inheritdoc} + */ +- public function getNamespace() ++ public function getNamespace(): string + { + return 'http://example.org/schema/dic/'.$this->getAlias(); +@@ -77,5 +77,5 @@ abstract class Extension implements ExtensionInterface, ConfigurationExtensionIn + * {@inheritdoc} + */ +- public function getConfiguration(array $config, ContainerBuilder $container) ++ public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + $class = static::class; +diff --git a/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php b/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php +index f2373ed5ea..1eec21a938 100644 +--- a/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php ++++ b/src/Symfony/Component/DependencyInjection/Extension/ExtensionInterface.php +@@ -33,5 +33,5 @@ interface ExtensionInterface + * @return string + */ +- public function getNamespace(); ++ public function getNamespace(): string; + + /** +@@ -40,5 +40,5 @@ interface ExtensionInterface + * @return string|false + */ +- public function getXsdValidationBasePath(); ++ public function getXsdValidationBasePath(): string|false; + + /** +@@ -49,4 +49,4 @@ interface ExtensionInterface + * @return string + */ +- public function getAlias(); ++ public function getAlias(): string; + } +diff --git a/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/InstantiatorInterface.php b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/InstantiatorInterface.php +index a9d78115dd..8b3b420a9c 100644 +--- a/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/InstantiatorInterface.php ++++ b/src/Symfony/Component/DependencyInjection/LazyProxy/Instantiator/InstantiatorInterface.php +@@ -31,4 +31,4 @@ interface InstantiatorInterface + * @return object + */ +- public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator); ++ public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object; + } +diff --git a/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php b/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php +index 2085e428e9..ca0d6964e5 100644 +--- a/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php ++++ b/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php +@@ -46,4 +46,4 @@ interface EventSubscriberInterface + * @return array> + */ +- public static function getSubscribedEvents(); ++ public static function getSubscribedEvents(): array; + } +diff --git a/src/Symfony/Component/ExpressionLanguage/ExpressionFunctionProviderInterface.php b/src/Symfony/Component/ExpressionLanguage/ExpressionFunctionProviderInterface.php +index 479aeef880..272954c082 100644 +--- a/src/Symfony/Component/ExpressionLanguage/ExpressionFunctionProviderInterface.php ++++ b/src/Symfony/Component/ExpressionLanguage/ExpressionFunctionProviderInterface.php +@@ -20,4 +20,4 @@ interface ExpressionFunctionProviderInterface + * @return ExpressionFunction[] + */ +- public function getFunctions(); ++ public function getFunctions(): array; + } +diff --git a/src/Symfony/Component/Form/AbstractExtension.php b/src/Symfony/Component/Form/AbstractExtension.php +index 5a077a42a6..62a234b116 100644 +--- a/src/Symfony/Component/Form/AbstractExtension.php ++++ b/src/Symfony/Component/Form/AbstractExtension.php +@@ -114,5 +114,5 @@ abstract class AbstractExtension implements FormExtensionInterface + * @return FormTypeInterface[] + */ +- protected function loadTypes() ++ protected function loadTypes(): array + { + return []; +@@ -134,5 +134,5 @@ abstract class AbstractExtension implements FormExtensionInterface + * @return FormTypeGuesserInterface|null + */ +- protected function loadTypeGuesser() ++ protected function loadTypeGuesser(): ?FormTypeGuesserInterface + { + return null; +diff --git a/src/Symfony/Component/Form/AbstractRendererEngine.php b/src/Symfony/Component/Form/AbstractRendererEngine.php +index 3dbe0a8420..b4179c785c 100644 +--- a/src/Symfony/Component/Form/AbstractRendererEngine.php ++++ b/src/Symfony/Component/Form/AbstractRendererEngine.php +@@ -135,5 +135,5 @@ abstract class AbstractRendererEngine implements FormRendererEngineInterface + * @return bool + */ +- abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName); ++ abstract protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName): bool; + + /** +diff --git a/src/Symfony/Component/Form/AbstractType.php b/src/Symfony/Component/Form/AbstractType.php +index 3325b8bc27..1cc22a1dab 100644 +--- a/src/Symfony/Component/Form/AbstractType.php ++++ b/src/Symfony/Component/Form/AbstractType.php +@@ -52,5 +52,5 @@ abstract class AbstractType implements FormTypeInterface + * {@inheritdoc} + */ +- public function getBlockPrefix() ++ public function getBlockPrefix(): string + { + return StringUtil::fqcnToBlockPrefix(static::class) ?: ''; +@@ -60,5 +60,5 @@ abstract class AbstractType implements FormTypeInterface + * {@inheritdoc} + */ +- public function getParent() ++ public function getParent(): ?string + { + return FormType::class; +diff --git a/src/Symfony/Component/Form/DataTransformerInterface.php b/src/Symfony/Component/Form/DataTransformerInterface.php +index 8495905e17..1e53be60be 100644 +--- a/src/Symfony/Component/Form/DataTransformerInterface.php ++++ b/src/Symfony/Component/Form/DataTransformerInterface.php +@@ -60,5 +60,5 @@ interface DataTransformerInterface + * @throws TransformationFailedException when the transformation fails + */ +- public function transform(mixed $value); ++ public function transform(mixed $value): mixed; + + /** +@@ -89,4 +89,4 @@ interface DataTransformerInterface + * @throws TransformationFailedException when the transformation fails + */ +- public function reverseTransform(mixed $value); ++ public function reverseTransform(mixed $value): mixed; + } +diff --git a/src/Symfony/Component/Form/FormRendererEngineInterface.php b/src/Symfony/Component/Form/FormRendererEngineInterface.php +index aa249270a0..3c9d04ff9a 100644 +--- a/src/Symfony/Component/Form/FormRendererEngineInterface.php ++++ b/src/Symfony/Component/Form/FormRendererEngineInterface.php +@@ -131,4 +131,4 @@ interface FormRendererEngineInterface + * @return string + */ +- public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []); ++ public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string; + } +diff --git a/src/Symfony/Component/Form/FormTypeGuesserInterface.php b/src/Symfony/Component/Form/FormTypeGuesserInterface.php +index 61e2c5f80d..4d6b335474 100644 +--- a/src/Symfony/Component/Form/FormTypeGuesserInterface.php ++++ b/src/Symfony/Component/Form/FormTypeGuesserInterface.php +@@ -22,5 +22,5 @@ interface FormTypeGuesserInterface + * @return Guess\TypeGuess|null + */ +- public function guessType(string $class, string $property); ++ public function guessType(string $class, string $property): ?Guess\TypeGuess; + + /** +@@ -29,5 +29,5 @@ interface FormTypeGuesserInterface + * @return Guess\ValueGuess|null + */ +- public function guessRequired(string $class, string $property); ++ public function guessRequired(string $class, string $property): ?Guess\ValueGuess; + + /** +@@ -36,5 +36,5 @@ interface FormTypeGuesserInterface + * @return Guess\ValueGuess|null + */ +- public function guessMaxLength(string $class, string $property); ++ public function guessMaxLength(string $class, string $property): ?Guess\ValueGuess; + + /** +@@ -50,4 +50,4 @@ interface FormTypeGuesserInterface + * @return Guess\ValueGuess|null + */ +- public function guessPattern(string $class, string $property); ++ public function guessPattern(string $class, string $property): ?Guess\ValueGuess; + } +diff --git a/src/Symfony/Component/Form/FormTypeInterface.php b/src/Symfony/Component/Form/FormTypeInterface.php +index 2b9066a511..1c9e9f5a26 100644 +--- a/src/Symfony/Component/Form/FormTypeInterface.php ++++ b/src/Symfony/Component/Form/FormTypeInterface.php +@@ -77,5 +77,5 @@ interface FormTypeInterface + * @return string + */ +- public function getBlockPrefix(); ++ public function getBlockPrefix(): string; + + /** +@@ -84,4 +84,4 @@ interface FormTypeInterface + * @return string|null + */ +- public function getParent(); ++ public function getParent(): ?string; + } +diff --git a/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerInterface.php b/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerInterface.php +index 1f1740b7e2..22dc8ea26a 100644 +--- a/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerInterface.php ++++ b/src/Symfony/Component/HttpKernel/CacheWarmer/CacheWarmerInterface.php +@@ -29,4 +29,4 @@ interface CacheWarmerInterface extends WarmableInterface + * @return bool + */ +- public function isOptional(); ++ public function isOptional(): bool; + } +diff --git a/src/Symfony/Component/HttpKernel/CacheWarmer/WarmableInterface.php b/src/Symfony/Component/HttpKernel/CacheWarmer/WarmableInterface.php +index 2f442cb536..d98909cfae 100644 +--- a/src/Symfony/Component/HttpKernel/CacheWarmer/WarmableInterface.php ++++ b/src/Symfony/Component/HttpKernel/CacheWarmer/WarmableInterface.php +@@ -24,4 +24,4 @@ interface WarmableInterface + * @return string[] A list of classes or files to preload on PHP 7.4+ + */ +- public function warmUp(string $cacheDir); ++ public function warmUp(string $cacheDir): array; + } +diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php +index 1289cf0ea9..dabeec97ee 100644 +--- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php ++++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollector.php +@@ -58,5 +58,5 @@ abstract class DataCollector implements DataCollectorInterface + * @return callable[] The casters to add to the cloner + */ +- protected function getCasters() ++ protected function getCasters(): array + { + $casters = [ +diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php b/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php +index 1cb865fd66..f6f4efe7a7 100644 +--- a/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php ++++ b/src/Symfony/Component/HttpKernel/DataCollector/DataCollectorInterface.php +@@ -33,4 +33,4 @@ interface DataCollectorInterface extends ResetInterface + * @return string + */ +- public function getName(); ++ public function getName(): string; + } +diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +index eeec55593f..c12df944fc 100644 +--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php ++++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +@@ -448,5 +448,5 @@ class HttpCache implements HttpKernelInterface, TerminableInterface + * @return Response + */ +- protected function forward(Request $request, bool $catch = false, Response $entry = null) ++ protected function forward(Request $request, bool $catch = false, Response $entry = null): Response + { + if ($this->surrogate) { +diff --git a/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php b/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php +index fc70d8970d..9dde6d1f93 100644 +--- a/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php ++++ b/src/Symfony/Component/HttpKernel/HttpKernelBrowser.php +@@ -61,5 +61,5 @@ class HttpKernelBrowser extends AbstractBrowser + * @return Response + */ +- protected function doRequest(object $request) ++ protected function doRequest(object $request): Response + { + $response = $this->kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, $this->catchExceptions); +@@ -79,5 +79,5 @@ class HttpKernelBrowser extends AbstractBrowser + * @return string + */ +- protected function getScript(object $request) ++ protected function getScript(object $request): string + { + $kernel = var_export(serialize($this->kernel), true); +diff --git a/src/Symfony/Component/HttpKernel/Log/DebugLoggerInterface.php b/src/Symfony/Component/HttpKernel/Log/DebugLoggerInterface.php +index 19ff0db181..f0f4a5829f 100644 +--- a/src/Symfony/Component/HttpKernel/Log/DebugLoggerInterface.php ++++ b/src/Symfony/Component/HttpKernel/Log/DebugLoggerInterface.php +@@ -30,5 +30,5 @@ interface DebugLoggerInterface + * @return array + */ +- public function getLogs(Request $request = null); ++ public function getLogs(Request $request = null): array; + + /** +@@ -37,5 +37,5 @@ interface DebugLoggerInterface + * @return int + */ +- public function countErrors(Request $request = null); ++ public function countErrors(Request $request = null): int; + + /** +diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php +index fe77644762..09dcfe166b 100644 +--- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php ++++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php +@@ -486,5 +486,5 @@ class OptionsResolver implements Options + * @throws AccessException If called from a lazy option or normalizer + */ +- public function setNormalizer(string $option, \Closure $normalizer) ++ public function setNormalizer(string $option, \Closure $normalizer): static + { + if ($this->locked) { +@@ -570,5 +570,5 @@ class OptionsResolver implements Options + * @throws AccessException If called from a lazy option or normalizer + */ +- public function setAllowedValues(string $option, mixed $allowedValues) ++ public function setAllowedValues(string $option, mixed $allowedValues): static + { + if ($this->locked) { +@@ -610,5 +610,5 @@ class OptionsResolver implements Options + * @throws AccessException If called from a lazy option or normalizer + */ +- public function addAllowedValues(string $option, mixed $allowedValues) ++ public function addAllowedValues(string $option, mixed $allowedValues): static + { + if ($this->locked) { +@@ -650,5 +650,5 @@ class OptionsResolver implements Options + * @throws AccessException If called from a lazy option or normalizer + */ +- public function setAllowedTypes(string $option, string|array $allowedTypes) ++ public function setAllowedTypes(string $option, string|array $allowedTypes): static + { + if ($this->locked) { +@@ -684,5 +684,5 @@ class OptionsResolver implements Options + * @throws AccessException If called from a lazy option or normalizer + */ +- public function addAllowedTypes(string $option, string|array $allowedTypes) ++ public function addAllowedTypes(string $option, string|array $allowedTypes): static + { + if ($this->locked) { +diff --git a/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php b/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php +index fbb37d9f94..522e0487a9 100644 +--- a/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php ++++ b/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php +@@ -31,5 +31,5 @@ interface PropertyPathInterface extends \Traversable + * @return int + */ +- public function getLength(); ++ public function getLength(): int; + + /** +@@ -43,5 +43,5 @@ interface PropertyPathInterface extends \Traversable + * @return self|null + */ +- public function getParent(); ++ public function getParent(): ?\Symfony\Component\PropertyAccess\PropertyPathInterface; + + /** +@@ -50,5 +50,5 @@ interface PropertyPathInterface extends \Traversable + * @return list + */ +- public function getElements(); ++ public function getElements(): array; + + /** +@@ -61,5 +61,5 @@ interface PropertyPathInterface extends \Traversable + * @throws Exception\OutOfBoundsException If the offset is invalid + */ +- public function getElement(int $index); ++ public function getElement(int $index): string; + + /** +@@ -72,5 +72,5 @@ interface PropertyPathInterface extends \Traversable + * @throws Exception\OutOfBoundsException If the offset is invalid + */ +- public function isProperty(int $index); ++ public function isProperty(int $index): bool; + + /** +@@ -83,4 +83,4 @@ interface PropertyPathInterface extends \Traversable + * @throws Exception\OutOfBoundsException If the offset is invalid + */ +- public function isIndex(int $index); ++ public function isIndex(int $index): bool; + } +diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +index cfffdb2f71..3e2261898a 100644 +--- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php ++++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +@@ -725,5 +725,5 @@ class PropertyAccessorTest extends TestCase + * @return mixed + */ +- public function getFoo() ++ public function getFoo(): mixed + { + return $this->foo; +diff --git a/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php +index f9ee787130..61f8b6d5be 100644 +--- a/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php ++++ b/src/Symfony/Component/PropertyInfo/PropertyAccessExtractorInterface.php +@@ -24,5 +24,5 @@ interface PropertyAccessExtractorInterface + * @return bool|null + */ +- public function isReadable(string $class, string $property, array $context = []); ++ public function isReadable(string $class, string $property, array $context = []): ?bool; + + /** +@@ -31,4 +31,4 @@ interface PropertyAccessExtractorInterface + * @return bool|null + */ +- public function isWritable(string $class, string $property, array $context = []); ++ public function isWritable(string $class, string $property, array $context = []): ?bool; + } +diff --git a/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php +index 326e6cccb3..ae7c6b612b 100644 +--- a/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php ++++ b/src/Symfony/Component/PropertyInfo/PropertyListExtractorInterface.php +@@ -24,4 +24,4 @@ interface PropertyListExtractorInterface + * @return string[]|null + */ +- public function getProperties(string $class, array $context = []); ++ public function getProperties(string $class, array $context = []): ?array; + } +diff --git a/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php b/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php +index 6da0bcb4c8..16e9765b1d 100644 +--- a/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php ++++ b/src/Symfony/Component/PropertyInfo/PropertyTypeExtractorInterface.php +@@ -24,4 +24,4 @@ interface PropertyTypeExtractorInterface + * @return Type[]|null + */ +- public function getTypes(string $class, string $property, array $context = []); ++ public function getTypes(string $class, string $property, array $context = []): ?array; + } +diff --git a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +index 2dd0e5efbf..95e01d8955 100644 +--- a/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php ++++ b/src/Symfony/Component/Routing/Loader/AnnotationClassLoader.php +@@ -260,5 +260,5 @@ abstract class AnnotationClassLoader implements LoaderInterface + * @return string + */ +- protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) ++ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method): string + { + $name = str_replace('\\', '_', $class->name).'_'.$method->name; +diff --git a/src/Symfony/Component/Routing/Router.php b/src/Symfony/Component/Routing/Router.php +index be653e4f00..e46300d474 100644 +--- a/src/Symfony/Component/Routing/Router.php ++++ b/src/Symfony/Component/Routing/Router.php +@@ -178,5 +178,5 @@ class Router implements RouterInterface, RequestMatcherInterface + * {@inheritdoc} + */ +- public function getRouteCollection() ++ public function getRouteCollection(): RouteCollection + { + if (null === $this->collection) { +diff --git a/src/Symfony/Component/Routing/RouterInterface.php b/src/Symfony/Component/Routing/RouterInterface.php +index 6912f8a15b..caf18c886a 100644 +--- a/src/Symfony/Component/Routing/RouterInterface.php ++++ b/src/Symfony/Component/Routing/RouterInterface.php +@@ -32,4 +32,4 @@ interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface + * @return RouteCollection + */ +- public function getRouteCollection(); ++ public function getRouteCollection(): RouteCollection; + } +diff --git a/src/Symfony/Component/Security/Core/Authentication/RememberMe/TokenProviderInterface.php b/src/Symfony/Component/Security/Core/Authentication/RememberMe/TokenProviderInterface.php +index eda4730004..00cfc5b9c7 100644 +--- a/src/Symfony/Component/Security/Core/Authentication/RememberMe/TokenProviderInterface.php ++++ b/src/Symfony/Component/Security/Core/Authentication/RememberMe/TokenProviderInterface.php +@@ -28,5 +28,5 @@ interface TokenProviderInterface + * @throws TokenNotFoundException if the token is not found + */ +- public function loadTokenBySeries(string $series); ++ public function loadTokenBySeries(string $series): PersistentTokenInterface; + + /** +diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php b/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php +index 7e401c3ff3..6b446ff376 100644 +--- a/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php ++++ b/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php +@@ -36,4 +36,4 @@ interface VoterInterface + * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED + */ +- public function vote(TokenInterface $token, mixed $subject, array $attributes); ++ public function vote(TokenInterface $token, mixed $subject, array $attributes): int; + } +diff --git a/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php b/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php +index 9e2b02b603..c6a753f1f7 100644 +--- a/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php ++++ b/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php +@@ -89,5 +89,5 @@ class AuthenticationException extends RuntimeException + * @return string + */ +- public function getMessageKey() ++ public function getMessageKey(): string + { + return 'An authentication exception occurred.'; +diff --git a/src/Symfony/Component/Security/Core/User/UserProviderInterface.php b/src/Symfony/Component/Security/Core/User/UserProviderInterface.php +index ec90d413fa..9f1401aa91 100644 +--- a/src/Symfony/Component/Security/Core/User/UserProviderInterface.php ++++ b/src/Symfony/Component/Security/Core/User/UserProviderInterface.php +@@ -45,5 +45,5 @@ interface UserProviderInterface + * @throws UserNotFoundException if the user is not found + */ +- public function refreshUser(UserInterface $user); ++ public function refreshUser(UserInterface $user): UserInterface; + + /** +@@ -52,5 +52,5 @@ interface UserProviderInterface + * @return bool + */ +- public function supportsClass(string $class); ++ public function supportsClass(string $class): bool; + + /** +diff --git a/src/Symfony/Component/Security/Http/EntryPoint/AuthenticationEntryPointInterface.php b/src/Symfony/Component/Security/Http/EntryPoint/AuthenticationEntryPointInterface.php +index 91271d14a3..100c2fb549 100644 +--- a/src/Symfony/Component/Security/Http/EntryPoint/AuthenticationEntryPointInterface.php ++++ b/src/Symfony/Component/Security/Http/EntryPoint/AuthenticationEntryPointInterface.php +@@ -43,4 +43,4 @@ interface AuthenticationEntryPointInterface + * @return Response + */ +- public function start(Request $request, AuthenticationException $authException = null); ++ public function start(Request $request, AuthenticationException $authException = null): Response; + } +diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php +index 546b77d22f..24038d4b94 100644 +--- a/src/Symfony/Component/Security/Http/Firewall.php ++++ b/src/Symfony/Component/Security/Http/Firewall.php +@@ -106,5 +106,5 @@ class Firewall implements EventSubscriberInterface + * {@inheritdoc} + */ +- public static function getSubscribedEvents() ++ public static function getSubscribedEvents(): array + { + return [ +diff --git a/src/Symfony/Component/Security/Http/FirewallMapInterface.php b/src/Symfony/Component/Security/Http/FirewallMapInterface.php +index 480ea8ad6b..fa43d6a6e9 100644 +--- a/src/Symfony/Component/Security/Http/FirewallMapInterface.php ++++ b/src/Symfony/Component/Security/Http/FirewallMapInterface.php +@@ -38,4 +38,4 @@ interface FirewallMapInterface + * @return array{iterable, ExceptionListener, LogoutListener} + */ +- public function getListeners(Request $request); ++ public function getListeners(Request $request): array; + } +diff --git a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php +index 84a84ad1f3..6f66b6d32a 100644 +--- a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php ++++ b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php +@@ -35,5 +35,5 @@ interface DecoderInterface + * @throws UnexpectedValueException + */ +- public function decode(string $data, string $format, array $context = []); ++ public function decode(string $data, string $format, array $context = []): mixed; + + /** +@@ -44,4 +44,4 @@ interface DecoderInterface + * @return bool + */ +- public function supportsDecoding(string $format); ++ public function supportsDecoding(string $format): bool; + } +diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +index 7f86ed8d78..cf084423ec 100644 +--- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php ++++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +@@ -223,5 +223,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn + * @return string[]|AttributeMetadataInterface[]|bool + */ +- protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false) ++ protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool + { + $allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES]; +@@ -273,5 +273,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn + * @return bool + */ +- protected function isAllowedAttribute(object|string $classOrObject, string $attribute, string $format = null, array $context = []) ++ protected function isAllowedAttribute(object|string $classOrObject, string $attribute, string $format = null, array $context = []): bool + { + $ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES]; +@@ -324,5 +324,5 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn + * @throws MissingConstructorArgumentsException + */ +- protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null) ++ protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null): object + { + if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) { +diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +index a241215133..2ed1af7a02 100644 +--- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php ++++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +@@ -136,5 +136,5 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer + * {@inheritdoc} + */ +- public function supportsNormalization(mixed $data, string $format = null) ++ public function supportsNormalization(mixed $data, string $format = null): bool + { + return \is_object($data) && !$data instanceof \Traversable; +@@ -144,5 +144,5 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer + * {@inheritdoc} + */ +- public function normalize(mixed $object, string $format = null, array $context = []) ++ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + if (!isset($context['cache_key'])) { +@@ -277,5 +277,5 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer + * {@inheritdoc} + */ +- protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null) ++ protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null): object + { + if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { +@@ -339,5 +339,5 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer + * @return string[] + */ +- abstract protected function extractAttributes(object $object, string $format = null, array $context = []); ++ abstract protected function extractAttributes(object $object, string $format = null, array $context = []): array; + + /** +@@ -346,10 +346,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer + * @return mixed + */ +- abstract protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []); ++ abstract protected function getAttributeValue(object $object, string $attribute, string $format = null, array $context = []): mixed; + + /** + * {@inheritdoc} + */ +- public function supportsDenormalization(mixed $data, string $type, string $format = null) ++ public function supportsDenormalization(mixed $data, string $type, string $format = null): bool + { + return class_exists($type) || (interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type)); +@@ -359,5 +359,5 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer + * {@inheritdoc} + */ +- public function denormalize(mixed $data, string $type, string $format = null, array $context = []) ++ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed + { + if (!isset($context['cache_key'])) { +diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php +index 5e94400b80..726d89cbb1 100644 +--- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php ++++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php +@@ -45,5 +45,5 @@ interface DenormalizerInterface + * @throws ExceptionInterface Occurs for all the other cases of errors + */ +- public function denormalize(mixed $data, string $type, string $format = null, array $context = []); ++ public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed; + + /** +@@ -56,4 +56,4 @@ interface DenormalizerInterface + * @return bool + */ +- public function supportsDenormalization(mixed $data, string $type, string $format = null); ++ public function supportsDenormalization(mixed $data, string $type, string $format = null): bool; + } +diff --git a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php +index 30eeafb47b..a7a60ad2f2 100644 +--- a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php ++++ b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php +@@ -37,5 +37,5 @@ interface NormalizerInterface + * @throws ExceptionInterface Occurs for all the other cases of errors + */ +- public function normalize(mixed $object, string $format = null, array $context = []); ++ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null; + + /** +@@ -47,4 +47,4 @@ interface NormalizerInterface + * @return bool + */ +- public function supportsNormalization(mixed $data, string $format = null); ++ public function supportsNormalization(mixed $data, string $format = null): bool; + } +diff --git a/src/Symfony/Component/Templating/Helper/HelperInterface.php b/src/Symfony/Component/Templating/Helper/HelperInterface.php +index 5dade65db5..db0d0a00ea 100644 +--- a/src/Symfony/Component/Templating/Helper/HelperInterface.php ++++ b/src/Symfony/Component/Templating/Helper/HelperInterface.php +@@ -24,5 +24,5 @@ interface HelperInterface + * @return string + */ +- public function getName(); ++ public function getName(): string; + + /** +diff --git a/src/Symfony/Component/Translation/Extractor/AbstractFileExtractor.php b/src/Symfony/Component/Translation/Extractor/AbstractFileExtractor.php +index 4c088b94f9..86107a636d 100644 +--- a/src/Symfony/Component/Translation/Extractor/AbstractFileExtractor.php ++++ b/src/Symfony/Component/Translation/Extractor/AbstractFileExtractor.php +@@ -59,9 +59,9 @@ abstract class AbstractFileExtractor + * @return bool + */ +- abstract protected function canBeExtracted(string $file); ++ abstract protected function canBeExtracted(string $file): bool; + + /** + * @return iterable + */ +- abstract protected function extractFromDirectory(string|array $resource); ++ abstract protected function extractFromDirectory(string|array $resource): iterable; + } +diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php +index 46432f2f4c..47ac39d5e3 100644 +--- a/src/Symfony/Component/Validator/Constraint.php ++++ b/src/Symfony/Component/Validator/Constraint.php +@@ -243,5 +243,5 @@ abstract class Constraint + * @see __construct() + */ +- public function getDefaultOption() ++ public function getDefaultOption(): ?string + { + return null; +@@ -257,5 +257,5 @@ abstract class Constraint + * @see __construct() + */ +- public function getRequiredOptions() ++ public function getRequiredOptions(): array + { + return []; +@@ -271,5 +271,5 @@ abstract class Constraint + * @return string + */ +- public function validatedBy() ++ public function validatedBy(): string + { + return static::class.'Validator'; +@@ -285,5 +285,5 @@ abstract class Constraint + * @return string|string[] One or more constant values + */ +- public function getTargets() ++ public function getTargets(): string|array + { + return self::PROPERTY_CONSTRAINT; diff --git a/.github/get-modified-packages.php b/.github/get-modified-packages.php new file mode 100644 index 0000000000000..a3682af0b9d1a --- /dev/null +++ b/.github/get-modified-packages.php @@ -0,0 +1,73 @@ + $_SERVER['argc']) { + echo "Usage: app-packages modified-files\n"; + exit(1); +} + +$allPackages = json_decode($_SERVER['argv'][1], true, 512, \JSON_THROW_ON_ERROR); +$modifiedFiles = json_decode($_SERVER['argv'][2], true, 512, \JSON_THROW_ON_ERROR); + +// Sort to get the longest name first (match bridge not component) +usort($allPackages, function($a, $b) { + return strlen($b) <=> strlen($a) ?: $a <=> $b; +}); + +function getPackageType(string $packageDir): string +{ + if (preg_match('@Symfony/Bridge/@', $packageDir)) { + return 'bridge'; + } + + if (preg_match('@Symfony/Bundle/@', $packageDir)) { + return 'bundle'; + } + + if (preg_match('@Symfony/Component/[^/]+/Bridge/@', $packageDir)) { + return 'component_bridge'; + } + + if (preg_match('@Symfony/Component/@', $packageDir)) { + return 'component'; + } + + if (preg_match('@Symfony/Contracts/@', $packageDir)) { + return 'contract'; + } + + if (preg_match('@Symfony/Contracts$@', $packageDir)) { + return 'contracts'; + } + + throw new \LogicException(); +} + +$newPackage = []; +$modifiedPackages = []; +foreach ($modifiedFiles as $file) { + foreach ($allPackages as $package) { + if (0 === strpos($file, $package)) { + $modifiedPackages[$package] = true; + if ('LICENSE' === substr($file, -7)) { + /* + * There is never a reason to modify the LICENSE file, this diff + * must be adding a new package + */ + $newPackage[$package] = true; + } + break; + } + } +} + +$output = []; +foreach ($modifiedPackages as $directory => $bool) { + $name = json_decode(file_get_contents($directory.'/composer.json'), true)['name'] ?? 'unknown'; + $output[] = ['name' => $name, 'directory' => $directory, 'new' => $newPackage[$directory] ?? false, 'type' => getPackageType($directory)]; +} + +echo json_encode($output); diff --git a/.github/patch-types.php b/.github/patch-types.php index a308eda397d51..86b1c95c72ec5 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -7,15 +7,6 @@ require __DIR__.'/../.phpunit/phpunit/vendor/autoload.php'; -file_put_contents(__DIR__.'/../vendor/autoload.php', preg_replace('/^return (Composer.*);/m', <<<'EOTXT' -$loader = \1; -$loader->addClassMap(['Symfony\Component\Debug\Exception\FlattenException' => \dirname(__DIR__).'/src/Symfony/Component/Debug/Exception/FlattenException.php']); - -return $loader; - -EOTXT -, file_get_contents(__DIR__.'/../vendor/autoload.php'))); - $loader = require __DIR__.'/../vendor/autoload.php'; Symfony\Component\ErrorHandler\DebugClassLoader::enable(); @@ -29,21 +20,28 @@ case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/BadFileName.php'): case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php'): case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/ParseError.php'): - case false !== strpos($file, '/src/Symfony/Component/Debug/Tests/Fixtures/'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/intersectiontype_classes.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/MultipleArgumentsOptionalScalarNotReallyOptional.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/IntersectionConstructor.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/NewInInitializer.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ParentNotExists.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Preload/'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadClasses/MissingParent.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberIntersectionWithTrait.php'): case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'): - case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): - case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'): + case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Answer.php'): + case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Number.php'): + case false !== strpos($file, '/src/Symfony/Component/Form/Tests/Fixtures/Suit.php'): + case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php'): + case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'): + case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): + case false !== strpos($file, '/src/Symfony/Component/Validator/Tests/Fixtures/NestedAttribute/Entity.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/ReflectionIntersectionTypeFixture.php'): continue 2; diff --git a/.github/psalm/stubs/ForwardCompatTestTrait.php b/.github/psalm/stubs/ForwardCompatTestTrait.php deleted file mode 100644 index e3ddf4da3d431..0000000000000 --- a/.github/psalm/stubs/ForwardCompatTestTrait.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Test; - -use PHPUnit\Framework\TestCase; - -/** - * @internal - */ -trait ForwardCompatTestTrait -{ - private function doSetUp(): void - { - } - - private function doTearDown(): void - { - } - - protected function setUp(): void - { - $this->doSetUp(); - } - - protected function tearDown(): void - { - $this->doTearDown(); - } -} diff --git a/.github/psalm/stubs/SetUpTearDownTrait.php b/.github/psalm/stubs/SetUpTearDownTrait.php deleted file mode 100644 index 20dbe6fe73e0f..0000000000000 --- a/.github/psalm/stubs/SetUpTearDownTrait.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit; - -use PHPUnit\Framework\TestCase; - -trait SetUpTearDownTrait -{ - use Legacy\SetUpTearDownTraitForV8; -} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index cf6787a8d8e77..d3f97a643af4a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -20,9 +20,15 @@ jobs: strategy: matrix: - php: ['7.1', '8.0'] + php: ['8.0'] services: + postgres: + image: postgres:9.6-alpine + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: 'password' ldap: image: bitnami/openldap ports: @@ -66,6 +72,34 @@ jobs: image: rabbitmq:3.8.3 ports: - 5672:5672 + mongodb: + image: mongo + ports: + - 27017:27017 + couchbase: + image: couchbase:6.5.1 + ports: + - 8091:8091 + - 8092:8092 + - 8093:8093 + - 8094:8094 + - 11210:11210 + sqs: + image: asyncaws/testing-sqs + ports: + - 9494:9494 + zookeeper: + image: wurstmeister/zookeeper:3.4.6 + kafka: + image: wurstmeister/kafka:2.12-2.4.1 + ports: + - 9092:9092 + env: + KAFKA_AUTO_CREATE_TOPICS_ENABLE: false + KAFKA_CREATE_TOPICS: 'test-topic:1:1:compact' + KAFKA_ADVERTISED_HOST_NAME: 127.0.0.1 + KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' + KAFKA_ADVERTISED_PORT: 9092 steps: - name: Checkout @@ -78,19 +112,32 @@ jobs: echo "::endgroup::" echo "::group::install tools & libraries" - sudo apt-get install redis-server + sudo apt-get install librdkafka-dev redis-server sudo -- sh -c 'echo unixsocket /var/run/redis/redis-server.sock >> /etc/redis/redis.conf' sudo -- sh -c 'echo unixsocketperm 777 >> /etc/redis/redis.conf' sudo service redis-server restart echo "::endgroup::" + - name: Configure Couchbase + run: | + curl -s -u 'username=Administrator&password=111111' -X POST http://localhost:8091/node/controller/setupServices -d 'services=kv%2Cn1ql%2Cindex%2Cfts' + curl -s -X POST http://localhost:8091/settings/web -d 'username=Administrator&password=111111&port=SAME' + curl -s -u Administrator:111111 -X POST http://localhost:8091/pools/default/buckets -d 'ramQuotaMB=100&bucketType=ephemeral&name=cache' + curl -s -u Administrator:111111 -X POST http://localhost:8091/pools/default -d 'memoryQuota=256' + - name: Setup PHP uses: shivammathur/setup-php@v2 with: coverage: "none" - extensions: "memcached,redis-5.3.4,xsl,ldap" + extensions: "json,couchbase,memcached,mongodb-1.10.0,redis-5.3.4,rdkafka,xsl,ldap" ini-values: date.timezone=Europe/Paris,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,apc.enable_cli=1,zend.assertions=1 php-version: "${{ matrix.php }}" + tools: pecl + + - name: Display versions + run: | + php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' + php -i - name: Load fixtures uses: docker://bitnami/openldap @@ -106,6 +153,7 @@ jobs: echo COMPOSER_ROOT_VERSION=$COMPOSER_ROOT_VERSION >> $GITHUB_ENV echo "::group::composer update" + composer require --dev --no-update mongodb/mongodb:"1.9.1@dev|^1.9.1@stable" composer update --no-progress --ansi echo "::endgroup::" @@ -122,6 +170,10 @@ jobs: REDIS_SENTINEL_SERVICE: redis_sentinel MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages + MESSENGER_SQS_DSN: "sqs://localhost:9494/messages?sslmode=disable&poll_timeout=0.01" + MESSENGER_SQS_FIFO_QUEUE_DSN: "sqs://localhost:9494/messages.fifo?sslmode=disable&poll_timeout=0.01" + KAFKA_BROKER: 127.0.0.1:9092 + POSTGRES_HOST: localhost #- name: Run HTTP push tests # if: matrix.php == '8.0' diff --git a/.github/workflows/intl-data-tests.yml b/.github/workflows/intl-data-tests.yml index bb54e306c3d4d..a598db020e652 100644 --- a/.github/workflows/intl-data-tests.yml +++ b/.github/workflows/intl-data-tests.yml @@ -46,7 +46,7 @@ jobs: coverage: "none" extensions: "zip,intl-${{env.SYMFONY_ICU_VERSION}}" ini-values: "memory_limit=-1" - php-version: "7.4" + php-version: "8.0" - name: Install dependencies run: | diff --git a/.github/workflows/package-tests.yml b/.github/workflows/package-tests.yml new file mode 100644 index 0000000000000..23b65286814a9 --- /dev/null +++ b/.github/workflows/package-tests.yml @@ -0,0 +1,100 @@ +name: Package + +on: + pull_request: + paths: + - src/** + +jobs: + verify: + name: Verify + runs-on: Ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Fetch branch from where the PR started + run: git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + + - name: Find packages + id: find-packages + run: echo "::set-output name=packages::$(php .github/get-modified-packages.php $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | jq -R -s -c 'split("\n")[:-1]') $(git diff --name-only origin/${{ github.base_ref }} HEAD | grep src/ | jq -R -s -c 'split("\n")[:-1]'))" + + - name: Verify meta files are correct + run: | + ok=0 + + _file_exist() { + if [ ! -f "${1}" ]; then + echo "File ${1} does not exist" + return 1 + fi + } + + _file_not_exist() { + if [ -f "${1}" ]; then + echo "File ${1} should not be here" + return 1 + fi + } + + _correct_license_file() { + FIRST_LINE="Copyright (c) $(date +"%Y") Fabien Potencier" + PACKAGE_FIRST_LINE=$(head -1 ${1}) + if [[ "$FIRST_LINE" != "$PACKAGE_FIRST_LINE" ]]; then + echo "First line of the license file is wrong. Maybe it is the wrong year?" + return 1 + fi + + TEMPLATE=$(tail -n +2 LICENSE) + PACKAGE_LICENSE=$(tail -n +2 ${1}) + if [[ "$TEMPLATE" != "$PACKAGE_LICENSE" ]]; then + echo "Wrong content in license file" + return 1 + fi + } + + json='${{ steps.find-packages.outputs.packages }}' + for package in $(echo "${json}" | jq -r '.[] | @base64'); do + _jq() { + echo ${package} | base64 --decode | jq -r ${1} + } + + DIR=$(_jq '.directory') + NAME=$(_jq '.name') + echo "::group::$NAME" + TYPE=$(_jq '.type') + localExit=0 + + if [ $TYPE != 'contract' ] && [ $TYPE != 'contracts' ]; then + _file_exist $DIR/.gitattributes || localExit=1 + fi + _file_exist $DIR/.gitignore || localExit=1 + _file_exist $DIR/CHANGELOG.md || localExit=1 + _file_exist $DIR/LICENSE || localExit=1 + if [ $TYPE != 'contract' ]; then + _file_exist $DIR/phpunit.xml.dist || localExit=1 + fi + _file_exist $DIR/README.md || localExit=1 + _file_not_exist $DIR/phpunit.xml || localExit=1 + + if [ $(_jq '.new') == true ]; then + echo "Verifying new package" + _correct_license_file $DIR/LICENSE || localExit=1 + + if [ $TYPE != 'component_bridge' ]; then + if [ ! $(cat composer.json | jq -e ".replace.\"$NAME\"|test(\"self.version\")") ]; then + echo "Composer.json's replace section needs to contain $NAME" + localExit=1 + fi + fi + fi + + ok=$(( $localExit || $ok )) + echo ::endgroup:: + if [ $localExit -ne 0 ]; then + echo "::error::$NAME failed" + fi + done + + exit $ok diff --git a/.github/workflows/phpunit-bridge.yml b/.github/workflows/phpunit-bridge.yml index e8a19360a8b32..c135d13e3df4a 100644 --- a/.github/workflows/phpunit-bridge.yml +++ b/.github/workflows/phpunit-bridge.yml @@ -29,7 +29,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: "none" - php-version: "5.5" + php-version: "7.1" - name: Lint - run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e ForV6 -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait | parallel -j 4 php -l {} + run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait | parallel -j 4 php -l {} diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 8631d6c6a2b3d..0b7e5b18c658e 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -21,7 +21,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.1' - extensions: "json,memcached,mongodb,redis,xsl,ldap,dom" + extensions: "json,couchbase,memcached,mongodb,redis,xsl,ldap,dom" ini-values: "memory_limit=-1" coverage: none @@ -45,10 +45,6 @@ jobs: run: | git checkout composer.json git checkout -m ${{ github.base_ref }} - cat .github/psalm/stubs/SetUpTearDownTrait.php > src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php - cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Bundle\\FrameworkBundle/' > src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php - cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Component\\Form/' > src/Symfony/Component/Form/Test/ForwardCompatTestTrait.php - cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Component\\Validator/' > src/Symfony/Component/Validator/Test/ForwardCompatTestTrait.php ./vendor/bin/psalm.phar --set-baseline=.github/psalm/psalm.baseline.xml --no-progress git checkout -m FETCH_HEAD diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 6be35796056d5..8dfcf9c865571 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -23,8 +23,7 @@ jobs: strategy: matrix: include: - - php: '7.2' - - php: '7.4' + - php: '8.1' - php: '8.0' mode: high-deps - php: '8.1' @@ -142,11 +141,12 @@ jobs: - name: Patch return types if: "matrix.php == '8.1' && ! matrix.mode" run: | - sed -i 's/"\*\*\/Tests\/"//' composer.json + patch -sp1 < .github/expected-missing-return-types.diff + git add . composer install -q --optimize-autoloader - SYMFONY_PATCH_TYPE_DECLARATIONS=force=1 php .github/patch-types.php - SYMFONY_PATCH_TYPE_DECLARATIONS=force=1 php .github/patch-types.php # ensure the script is idempotent - echo PHPUNIT="$PHPUNIT,legacy" >> $GITHUB_ENV + SYMFONY_PATCH_TYPE_DECLARATIONS='force=2&php=8.0' php .github/patch-types.php + SYMFONY_PATCH_TYPE_DECLARATIONS='force=2&php=8.0' php .github/patch-types.php # ensure the script is idempotent + git diff --exit-code - name: Run tests run: | @@ -184,12 +184,14 @@ jobs: fi (cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) + (cd src/Symfony/Component/Lock; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) # matrix.mode = high-deps echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP && $PHPUNIT$LEGACY'" || X=1 # get a list of the patched components (relies on .github/build-packages.php being called in the previous step) (cd src/Symfony/Component/HttpFoundation; mv composer.bak composer.json) + (cd src/Symfony/Component/Lock; mv composer.bak composer.json) PATCHED_COMPONENTS=$(git diff --name-only src/ | grep composer.json || true) # for 5.4 LTS, checkout and test previous major with the patched components (only for patched components) @@ -203,6 +205,7 @@ jobs: git checkout -m FETCH_HEAD PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true) (cd src/Symfony/Component/HttpFoundation; composer require --dev --no-update mongodb/mongodb) + (cd src/Symfony/Component/Lock; composer require --dev --no-update mongodb/mongodb) if [[ $PATCHED_COMPONENTS ]]; then echo "::group::install phpunit" ./phpunit install @@ -219,12 +222,12 @@ jobs: script -e -c './phpunit --group tty' /dev/null - name: Run tests with SIGCHLD enabled PHP - if: "matrix.php == '7.2' && ! matrix.mode" + if: "matrix.php == '8.0' && ! matrix.mode" run: | mkdir build cd build - wget -q https://github.com/symfony/binary-utils/releases/download/v0.1/php-7.2.5-pcntl-sigchild.tar.bz2 - tar -xjf php-7.2.5-pcntl-sigchild.tar.bz2 + wget -q https://github.com/symfony/binary-utils/releases/download/v0.1/php-8.0.2-pcntl-sigchild.tar.bz2 + tar -xjf php-8.0.2-pcntl-sigchild.tar.bz2 cd .. ./build/php/bin/php ./phpunit --colors=always src/Symfony/Component/Process diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 3e8de34c124cb..5084a871f4247 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -28,6 +28,7 @@ 'Symfony/Bundle/FrameworkBundle/Resources/views/Form', // explicit trigger_error tests 'Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/', + 'Symfony/Component/Intl/Resources/data/', ]) // Support for older PHPunit version ->notPath('Symfony/Bridge/PhpUnit/SymfonyTestsListener.php') @@ -35,11 +36,14 @@ ->notPath('#Symfony/Bridge/PhpUnit/.*Legacy#') // file content autogenerated by `var_export` ->notPath('Symfony/Component/Translation/Tests/fixtures/resources.php') + // file content autogenerated by `VarExporter::export` + ->notPath('Symfony/Component/Serializer/Tests/Fixtures/serializer.class.metadata.php') // test template ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') // explicit trigger_error tests - ->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php') ->notPath('Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php') + // stop removing spaces on the end of the line in strings + ->notPath('Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php') ) ->setCacheFile('.php-cs-fixer.cache') ; diff --git a/.travis.yml b/.travis.yml index 2836521932ddf..04364c67c5369 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ addons: matrix: include: - - php: 7.3 + - php: 8.0 fast_finish: true cache: diff --git a/CHANGELOG-4.0.md b/CHANGELOG-4.0.md deleted file mode 100644 index cba2e57482bbb..0000000000000 --- a/CHANGELOG-4.0.md +++ /dev/null @@ -1,919 +0,0 @@ -CHANGELOG for 4.0.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 4.0 minor versions. - -To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash -To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.0.0...v4.0.1 - -* 4.0.14 (2018-08-01) - - * security #cve-2018-14774 [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer (nicolas-grekas) - * security #cve-2018-14773 [HttpFoundation] Remove support for legacy and risky HTTP headers (nicolas-grekas) - * bug #28003 [HttpKernel] Fixes invalid REMOTE_ADDR in inline subrequest when configuring trusted proxy with subnet (netiul) - * bug #28007 [FrameworkBundle] fixed guard event names for transitions (destillat) - * bug #28045 [HttpFoundation] Fix Cookie::isCleared (ro0NL) - * bug #28080 [HttpFoundation] fixed using _method parameter with invalid type (Phobetor) - * bug #28052 [HttpKernel] Fix merging bindings for controllers' locators (nicolas-grekas) - -* 4.0.13 (2018-07-23) - - * bug #28005 [HttpKernel] Fixed templateExists on parse error of the template name (yceruto) - * bug #27997 Serbo-Croatian has Serbian plural rule (kylekatarnls) - * bug #26193 Fix false-positive deprecation notices for TranslationLoader and WriteCheckSessionHandler (iquito) - * bug #27941 [WebProfilerBundle] Fixed icon alignment issue using Bootstrap 4.1.2 (jmsche) - * bug #27937 [HttpFoundation] reset callback on StreamedResponse when setNotModified() is called (rubencm) - * bug #27927 [HttpFoundation] Suppress side effects in 'get' and 'has' methods of NamespacedAttributeBag (webnet-fr) - * bug #27923 [Form/Profiler] Massively reducing memory footprint of form profiling pages... (VincentChalnot) - * bug #27918 [Console] correctly return parameter's default value on "--" (seschwar) - * bug #27904 [Filesystem] fix lock file permissions (fritzmg) - * bug #27903 [Lock] fix lock file permissions (fritzmg) - * bug #27889 [Form] Replace .initialism with .text-uppercase. (vudaltsov) - * bug #27902 Fix the detection of the Process new argument (stof) - * bug #27885 [HttpFoundation] don't encode cookie name for BC (nicolas-grekas) - * bug #27782 [DI] Fix dumping ignore-on-uninitialized references to synthetic services (nicolas-grekas) - * bug #27435 [OptionResolver] resolve arrays (Doctrs) - * bug #27728 [TwigBridge] Fix missing path and separators in loader paths list on debug:twig output (yceruto) - * bug #27837 [PropertyInfo] Fix dock block lookup fallback loop (DerManoMann) - * bug #27758 [WebProfilerBundle] Prevent toolbar links color override by css (alcalyn) - * bug #27847 [Security] Fix accepting null as $uidKey in LdapUserProvider (louhde) - * bug #27834 [DI] Don't show internal service id on binding errors (nicolas-grekas) - * bug #27831 Check for Hyper terminal on all operating systems. (azjezz) - * bug #27794 Add color support for Hyper terminal . (azjezz) - * bug #27809 [HttpFoundation] Fix tests: new message for status 425 (dunglas) - * bug #27618 [PropertyInfo] added handling of nullable types in PhpDoc (oxan) - * bug #27659 [HttpKernel] Make AbstractTestSessionListener compatible with CookieClearingLogoutHandler (thewilkybarkid) - * bug #27752 [Cache] provider does not respect option maxIdLength with versioning enabled (Constantine Shtompel) - * bug #27776 [ProxyManagerBridge] Fix support of private services (bis) (nicolas-grekas) - * bug #27714 [HttpFoundation] fix session tracking counter (nicolas-grekas, dmaicher) - * bug #27747 [HttpFoundation] fix registration of session proxies (nicolas-grekas) - * bug #27722 Redesign the Debug error page in prod (javiereguiluz) - * bug #27716 [DI] fix dumping deprecated service in yaml (nicolas-grekas) - -* 4.0.12 (2018-06-25) - - * bug #27626 [TwigBundle][DX] Only add the Twig WebLinkExtension if the WebLink component is enabled (thewilkybarkid) - * bug #27701 [SecurityBundle] Dont throw if "security.http_utils" is not found (nicolas-grekas) - * bug #27690 [DI] Resolve env placeholder in logs (ro0NL) - * bug #26534 allow_extra_attributes does not throw an exception as documented (deviantintegral) - * bug #27668 [Lock] use 'r+' for fopen (fixes issue on Solaris) (fritzmg) - * bug #27669 [Filesystem] fix file lock on SunOS (fritzmg) - * bug #27662 [HttpKernel] fix handling of nested Error instances (xabbuh) - * bug #26845 [Config] Fixing GlobResource when inside phar archive (vworldat) - * bug #27382 [Form] Fix error when rendering a DateIntervalType form with exactly 0 weeks (krixon) - * bug #27309 Fix surrogate not using original request (Toflar) - * bug #27467 [HttpKernel] fix session tracking in surrogate master requests (nicolas-grekas) - * bug #27630 [Validator][Form] Remove BOM in some xlf files (gautierderuette) - * bug #27596 [Framework][Workflow] Added support for interfaces (vudaltsov) - * bug #27593 [ProxyManagerBridge] Fixed support of private services (nicolas-grekas) - * bug #27591 [VarDumper] Fix dumping ArrayObject and ArrayIterator instances (nicolas-grekas) - * bug #27581 Fix bad method call with guard authentication + session migration (weaverryan) - * bug #27576 [Cache] Fix expiry comparisons in array-based pools (nicolas-grekas) - * bug #27556 Avoiding session migration for stateless firewall UsernamePasswordJsonAuthenticationListener (weaverryan) - * bug #27452 Avoid migration on stateless firewalls (weaverryan) - * bug #27568 [DI] Deduplicate generated proxy classes (nicolas-grekas) - * bug #27326 [Serializer] deserialize from xml: Fix a collection that contains the only one element (webnet-fr) - * bug #27567 [PhpUnitBridge] Fix error on some Windows OS (Nsbx) - * bug #27357 [Lock] Remove released semaphore (jderusse) - * bug #27416 TagAwareAdapter over non-binary memcached connections corrupts memcache (Aleksey Prilipko) - * bug #27514 [Debug] Pass previous exception to FatalErrorException (pmontoya) - * bug #27516 Revert "bug #26138 [HttpKernel] Catch HttpExceptions when templating is not installed (cilefen)" (nicolas-grekas) - * bug #27318 [Cache] memcache connect should not add duplicate entries on sequential calls (Aleksey Prilipko) - * bug #27389 [Serializer] Fix serializer tries to denormalize null values on nullable properties (ogizanagi) - * bug #27272 [FrameworkBundle] Change priority of AddConsoleCommandPass to TYPE_BEFORE_REMOVING (upyx) - * bug #27396 [HttpKernel] fix registering IDE links (nicolas-grekas) - * bug #26973 [HttpKernel] Set first trusted proxy as REMOTE_ADDR in InlineFragmentRenderer. (kmadejski) - * bug #27303 [Process] Consider "executable" suffixes first on Windows (sanmai) - * bug #27297 Triggering RememberMe's loginFail() when token cannot be created (weaverryan) - * bug #27344 [HttpKernel] reset kernel start time on reboot (kiler129) - * bug #27365 [Serializer] Check the value of enable_max_depth if defined (dunglas) - * bug #27358 [PhpUnitBridge] silence some stderr outputs (ostrolucky) - * bug #27366 [DI] never inline lazy services (nicolas-grekas) - -* 4.0.11 (2018-05-25) - - * bug #27364 [DI] Fix bad exception on uninitialized references to non-shared services (nicolas-grekas) - * bug #27359 [HttpFoundation] Fix perf issue during MimeTypeGuesser intialization (nicolas-grekas) - * security #cve-2018-11408 [SecurityBundle] Fail if security.http_utils cannot be configured - * security #cve-2018-11406 clear CSRF tokens when the user is logged out - * security #cve-2018-11385 migrating session for UsernamePasswordJsonAuthenticationListener - * security #cve-2018-11385 Adding session authentication strategy to Guard to avoid session fixation - * security #cve-2018-11385 Adding session strategy to ALL listeners to avoid *any* possible fixation - * security #cve-2018-11386 [HttpFoundation] Break infinite loop in PdoSessionHandler when MySQL is in loose mode - * bug #27341 [WebProfilerBundle] Fixed validator/dump trace CSS (yceruto) - * bug #27337 [FrameworkBundle] fix typo in CacheClearCommand (emilielorenzo) - -* 4.0.10 (2018-05-21) - - * bug #27264 [Validator] Use strict type in URL validator (mimol91) - * bug #27267 [DependencyInjection] resolve array env vars (jamesthomasonjr) - * bug #26781 [Form] Fix precision of MoneyToLocalizedStringTransformer's divisions on transform() (syastrebov) - * bug #27286 [Translation] Add Occitan plural rule (kylekatarnls) - * bug #27271 [DI] Allow defining bindings on ChildDefinition (nicolas-grekas) - * bug #27246 Disallow invalid characters in session.name (ostrolucky) - * bug #27287 [PropertyInfo] fix resolving parent|self type hints (nicolas-grekas) - * bug #27281 [HttpKernel] Fix dealing with self/parent in ArgumentMetadataFactory (fabpot) - * bug #24805 [Security] Fix logout (MatTheCat) - * bug #27265 [DI] Shared services should not be inlined in non-shared ones (nicolas-grekas) - * bug #27141 [Process] Suppress warnings when open_basedir is non-empty (cbj4074) - * bug #27250 [Session] limiting :key for GET_LOCK to 64 chars (oleg-andreyev) - * bug #27237 [Debug] Fix populating error_get_last() for handled silent errors (nicolas-grekas) - * bug #27232 [Cache][Lock] Fix usages of error_get_last() (nicolas-grekas) - * bug #27236 [Filesystem] Fix usages of error_get_last() (nicolas-grekas) - * bug #27191 [DI] Display previous error messages when throwing unused bindings (nicolas-grekas) - * bug #27231 [FrameworkBundle] Fix cache:clear on vagrant (nicolas-grekas) - * bug #27222 [WebProfilerBundle][Cache] Fix misses calculation when calling getItems (fsevestre) - * bug #27227 [HttpKernel] Handle NoConfigurationException "onKernelException()" (nicolas-grekas) - * bug #27152 [HttpFoundation] use brace-style regex delimiters (xabbuh) - * bug #27158 [Cache] fix logic for fetching tag versions on TagAwareAdapter (dmaicher) - * bug #27143 [Console] By default hide the short exception trace line from exception messages in Symfony's commands (yceruto) - * bug #27133 [Doctrine Bridge] fix priority for doctrine event listeners (dmaicher) - * bug #27135 [FrameworkBundle] Use the correct service id for CachePoolPruneCommand in its compiler pass (DemonTPx) - * feature #24896 Add CODE_OF_CONDUCT.md (egircys) - -* 4.0.9 (2018-04-30) - - * bug #27074 [Debug][WebProfilerBundle] Fix setting file link format (lyrixx, nicolas-grekas) - * bug #27088 ResolveBindingsPass: Don't throw error for unused service, missing parent class (weaverryan) - * bug #27086 [PHPUnitBridge] Add an implementation just for php 7.0 (greg0ire) - * bug #26138 [HttpKernel] Catch HttpExceptions when templating is not installed (cilefen) - * bug #27007 [Cache] TagAwareAdapterInterface::invalidateTags() should commit deferred items (nicolas-grekas) - * bug #27067 [HttpFoundation] Fix setting session-related ini settings (e-moe) - * bug #27061 [HttpKernel] Don't clean legacy containers that are still loaded (nicolas-grekas) - * bug #27064 [VarDumper] Fix HtmlDumper classes match (ogizanagi) - * bug #27016 [Security][Guard] GuardAuthenticationProvider::authenticate cannot return null (biomedia-thomas) - * bug #26831 [Bridge/Doctrine] count(): Parameter must be an array or an object that implements Countable (gpenverne) - * bug #27044 [Security] Skip user checks if not implementing UserInterface (chalasr) - * bug #27025 [DI] Add check of internal type to ContainerBuilder::getReflectionClass (upyx) - * bug #26994 [PhpUnitBridge] Add type hints (greg0ire) - * bug #26014 [Security] Fixed being logged out on failed attempt in guard (iltar) - * bug #25348 [HttpFoundation] Send cookies using header() to fix "SameSite" ones (nicolas-grekas, cvilleger) - * bug #26910 Use new PHP7.2 functions in hasColorSupport (johnstevenson) - * bug #26999 [VarDumper] Fix dumping of SplObjectStorage (corphi) - * bug #25841 [DoctrineBridge] Fix bug when indexBy is meta key in PropertyInfo\DoctrineExtractor (insekticid) - * bug #26983 [TwigBridge] [Bootstrap 4] Fix PercentType error rendering. (alexismarquis) - * bug #26980 [TwigBundle] fix formatting arguments in plaintext format (xabbuh) - * bug #26886 Don't assume that file binary exists on *nix OS (teohhanhui) - * bug #26959 [Console] Fix PSR exception context key (scaytrase) - * bug #26899 [Routing] Fix loading multiple class annotations for invokable classes (1ed) - * bug #26643 Fix that ESI/SSI processing can turn a "private" response "public" (mpdude) - * bug #26932 [Form] Fixed trimming choice values (HeahDude) - * bug #26922 [TwigBundle] fix rendering exception stack traces (xabbuh) - * bug #26773 [HttpKernel] Make ServiceValueResolver work if controller namespace starts with a backslash in routing (mathieutu) - * bug #26870 Add d-block to bootstrap 4 alerts (Normunds) - * bug #26857 [HttpKernel] Dont create mock cookie for new sessions in tests (nicolas-grekas) - * bug #26875 [Console] Don't go past exact matches when autocompleting (nicolas-grekas) - * bug #26823 [Validator] Fix LazyLoadingMetadataFactory with PSR6Cache for non classname if tested values isn't existing class (Pascal Montoya, pmontoya) - * bug #26834 [Yaml] Throw parse error on unfinished inline map (nicolas-grekas) - -* 4.0.8 (2018-04-06) - - * bug #26802 [Security] register custom providers on ExpressionLanguage directly (dmaicher) - * bug #26794 [PhpUnitBridge] Catch deprecation error handler (cvilleger) - * bug #26788 [Security] Load the user before pre/post auth checks when needed (chalasr) - * bug #26792 [Routing] Fix throwing NoConfigurationException instead of 405 (nicolas-grekas) - * bug #26774 [SecurityBundle] Add missing argument to security.authentication.provider.simple (i3or1s, chalasr) - * bug #26763 [Finder] Remove duplicate slashes in filenames (helhum) - * bug #26758 [WebProfilerBundle][HttpKernel] Make FileLinkFormatter URL format generation lazy (nicolas-grekas) - -* 4.0.7 (2018-04-03) - - * bug #26387 [Yaml] Fix regression when trying to parse multiline (antograssiot) - * bug #26749 Add PHPDbg support to HTTP components (hkdobrev) - * bug #26609 [Console] Fix check of color support on Windows (mlocati) - * bug #26727 [HttpCache] Unlink tmp file on error (Chansig) - * bug #26675 [HttpKernel] DumpDataCollector: do not flush when a dumper is provided (ogizanagi) - * bug #26663 [TwigBridge] Fix rendering of currency by MoneyType (ro0NL) - * bug #26595 [DI] Do not suggest writing an implementation when multiple exist (chalasr) - * bug #26662 [DI] Fix hardcoded cache dir for warmups (nicolas-grekas) - * bug #26677 Support phpdbg SAPI in Debug::enable() (hkdobrev) - * bug #26600 [Routing] Fixed the importing of files using glob patterns that match multiple resources (skalpa) - * bug #26589 [Ldap] cast to string when checking empty passwords (ismail1432) - * bug #26626 [WebProfilerBundle] use the router to resolve file links (nicolas-grekas) - * bug #26634 [DI] Cleanup remainings from autoregistration (nicolas-grekas) - * bug #26635 [DI] Dont tell about autoregistration in strict autowiring mode (nicolas-grekas) - * bug #26621 [Form] no type errors with invalid submitted data types (xabbuh) - * bug #26612 [PHPunit] suite variable should be used (prisis) - * bug #26337 [Finder] Fixed leading/trailing / in filename (lyrixx) - * bug #26584 [TwigBridge] allow html5 compatible rendering of forms with null names (systemist) - * bug #24401 [Form] Change datetime to datetime-local for HTML5 datetime input (pierredup) - * bug #26513 [FrameworkBundle] Respect debug mode when warm up annotations (Strate) - * bug #26370 [Security] added userChecker to SimpleAuthenticationProvider (i3or1s) - * bug #26569 [BrowserKit] Fix cookie path handling when $domain is null (dunglas) - * bug #26273 [Security][Profiler] Display the original expression in 'Access decision log' (lyrixx) - * bug #26427 [DependencyInjection] fix regression when extending the Container class without a constructor (lsmith77) - * bug #26562 [Bridge\PhpUnit] Cannot autoload class "\Symfony\Bridge\PhpUnit\SymfonyTestsListener" (Jake Bishop) - * bug #26598 Fixes #26563 (open_basedir restriction in effect) (temperatur) - * bug #26568 [Debug] Reset previous exception handler earlier to prevent infinite loop (nicolas-grekas) - * bug #26590 Make sure form errors is valid HTML (Nyholm) - * bug #26567 [DoctrineBridge] Don't rely on ClassMetadataInfo->hasField in DoctrineOrmTypeGuesser anymore (fancyweb) - * feature #26408 Readd 'form_label_errors' block to disable errors on form labels (birkof) - * bug #26591 [TwigBridge] Make sure we always render errors. Eventhough labels are disabled (Nyholm) - * bug #26356 [FrameworkBundle] HttpCache is not longer abstract (lyrixx) - * bug #26548 [DomCrawler] Change bad wording in ChoiceFormField::untick (dunglas) - * bug #26482 [PhpUnitBridge] Ability to use different composer.json file (amcastror) - * bug #26443 [Fix][HttpFoundation] Fix the updating of timestamp in the MemcachedSessionHandler (Alessandro Loffredo) - * bug #26400 [Config] ReflectionClassResource check abstract class (andrey1s) - * bug #26433 [DomCrawler] extract(): fix a bug when the attribute list is empty (dunglas) - * bug #26041 Display the Welcome Page when there is no homepage defined (javiereguiluz) - * bug #26452 [Intl] Load locale aliases to support alias fallbacks (jakzal) - * bug #26450 [CssSelector] Fix CSS identifiers parsing - they can start with dash (jakubkulhan) - -* 4.0.6 (2018-03-05) - - * bug #26393 [DI] Skip resource tracking if disabled (chalasr) - * bug #26403 fix the handling of timestamp in the MongoDBSessionHandler (hjanuschka) - * bug #26355 [DI] Fix missing "id" normalization when dumping the container (nicolas-grekas) - * bug #26368 [WebProfilerBundle] Fix Debug toolbar breaks app (xkobal) - * bug #26369 Use fill instead of style for svg colors (rpkamp) - * bug #26358 [FrameworkBundle] Silence "Failed to remove directory" on cache:clear (nicolas-grekas) - -* 4.0.5 (2018-03-01) - - * bug #26327 [Form][WCAG] Errors sign for people that do not see colors (Nyholm) - * bug #26326 [Form][WCAG] Added role="presentation" on tables & removed bootstrap4 table (Nyholm) - * bug #26325 [Form][WCAG] Add hidden labels on date and time fields (Nyholm) - * bug #26338 [Debug] Keep previous errors of Error instances (Philipp91) - * bug #26328 [Form][WCAG] Fixed HTML errors (Nyholm) - * bug #26290 [FrameworkBundle] [Console][DX] add a warning when command is not found (Simperfit) - * bug #26318 [Routing] Fix GC control of PHP-DSL (nicolas-grekas) - * bug #26312 [Routing] Don't throw 405 when scheme requirement doesn't match (nicolas-grekas) - * bug #26275 Set controller without __invoke method from invokable class (Tobion) - * bug #26298 Fix ArrayInput::toString() for InputArgument::IS_ARRAY args (maximium) - * bug #26177 Update excluded_ajax_paths for sf4 (jenaye) - * bug #26289 [Security] Add missing use of Role (tony-tran) - * bug #26286 [Security] Add missing use for RoleInterface (tony-tran) - * bug #26265 [PropertyInfo] throw exception if docblock factory does not exist (xabbuh) - * bug #26247 [Translation] Process multiple segments within a single unit. (timewasted) - * bug #26254 fix custom radios/inputs for checkbox/radio type (mssimi) - * bug #26234 [FrameworkBundle] Add missing XML config for circular_reference_handler (dunglas) - * bug #26236 [PropertyInfo] ReflectionExtractor: give a chance to other extractors if no properties (dunglas) - * bug #26227 Add support for URL-like DSNs for the PdoSessionHandler (stof) - * bug #25557 [WebProfilerBundle] add a way to limit ajax request (Simperfit) - * bug #26088 [FrameworkBundle] Fix using annotation_reader in compiler pass to inject configured cache provider (Laizerox) - * bug #26157 [HttpKernel] Send new session cookie from AbstractTestSessionListener after session invalidation (rpkamp) - * bug #26230 [WebProfilerBundle] Fix anchor CSS (ro0NL) - * bug #26228 [HttpFoundation] Fix missing "throw" in JsonResponse (nicolas-grekas) - * bug #26211 [Console] Suppress warning from sapi_windows_vt100_support (adawolfa) - * bug #26176 Retro-fit proxy code to make it deterministic for older proxy manager implementations (lstrojny) - * bug #25787 Yaml parser regression with comments and non-strings (alexpott) - * bug #26156 Fixes #26136: Avoid emitting warning in hasParameterOption() (greg-1-anderson) - * bug #26183 [DI] Add null check for removeChild (changmin.keum) - * bug #26167 [TwigBridge] Apply some changes to support Bootstrap4-stable (mpiot, Nyholm) - * bug #26173 [Security] fix accessing request values (xabbuh) - * bug #26089 [PhpUnitBridge] Added support for PHPUnit 7 in Coverage Listener (lyrixx) - * bug #26170 [PHPUnit bridge] Avoid running the remove command without any packages (stof) - * bug #26159 created validator.tl.xlf for Form/Translations (ergiegonzaga) - * bug #26100 [Routing] Throw 405 instead of 404 when redirect is not possible (nicolas-grekas) - * bug #26119 [TwigBundle][WebProfilerBundle] Fix JS collision (ro0NL) - * bug #26040 [Process] Check PHP_BINDIR before $PATH in PhpExecutableFinder (nicolas-grekas) - * bug #26067 [YAML] Issue #26065: leading spaces in YAML multi-line string literals (tamc) - * bug #26012 Exit as late as possible (greg0ire) - * bug #26082 [Cache][WebProfiler] fix collecting cache stats with sub-requests + allow clearing calls (dmaicher) - * bug #26024 [PhpBridge] add PHPUnit 7 support to SymfonyTestsListener (shieldo) - * bug #26020 [Lock] Log already-locked errors as "notice" instead of "warning" (Simperfit) - * bug #26043 [Serialized] add context to serialize and deserialize (andrey1s) - * bug #26127 Deterministic time in cache items for reproducible builds (lstrojny) - * bug #26128 Make kernel build time optionally deterministic (lstrojny) - * bug #26117 isCsrfTokenValid() replace string by ?string (GaylordP) - * bug #26112 Env var maps to undefined constant. (dsmink) - * bug #26111 [Security] fix merge of 2.7 into 2.8 + add test case (dmaicher) - * bug #25893 [Console] Fix hasParameterOption / getParameterOption when used with multiple flags (greg-1-anderson) - * bug #25756 [TwigBundle] Register TwigBridge extensions first (fancyweb) - * bug #26051 [WebProfilerBundle] Fix sub request link (ro0NL) - * bug #25947 PhpDocExtractor::getTypes() throws fatal error when type omitted (Jared Farrish) - * bug #25940 [Form] keep the context when validating forms (xabbuh) - * bug #26057 [SecurityBundle] use libsodium to run Argon2i related tests (xabbuh) - * bug #25373 Use the PCRE_DOLLAR_ENDONLY modifier in route regexes (mpdude) - * bug #24435 [Form] Make sure errors are a part of the label on bootstrap 4 - this is a requirement for WCAG2 (Nyholm) - * bug #25762 [DependencyInjection] always call the parent class' constructor (xabbuh) - * bug #25976 [Config] Handle Service/EventSubscriberInterface in ReflectionClassResource (nicolas-grekas) - * bug #25989 [DI][Routing] Fix tracking of globbed resources (nicolas-grekas, sroze) - * bug #26009 [SecurityBundle] Allow remember-me factory creation when multiple user providers are configured. (iisisrael) - * bug #26010 [CssSelector] For AND operator, the left operand should have parentheses, not only right operand (Arnaud CHASSEUX) - * bug #26000 Fixed issue #25985 (KevinFrantz) - * bug #25996 Don't show wanna-be-private services as public in debug:container (chalasr) - * bug #25914 [HttpKernel] collect extension information as late as possible (xabbuh) - * bug #25981 [DI] Fix tracking of source class changes for lazy-proxies (nicolas-grekas) - * bug #25971 [Debug] Fix bad registration of exception handler, leading to mem leak (nicolas-grekas) - * bug #25962 [Routing] Fix trailing slash redirection for non-safe verbs (nicolas-grekas) - * bug #25948 [Form] Fixed empty data on expanded ChoiceType and FileType (HeahDude) - * bug #25978 Deterministic proxy names (lstrojny) - * bug #25972 support sapi_windows_vt100_support for php 7.2+ (jhdxr) - * bug #25744 [TwigBridge] Allow label translation to be safe (MatTheCat) - * bug #25932 Don't stop PSR-4 service discovery if a parent class is missing (derrabus) - -* 4.0.4 (2018-01-29) - - * bug #25922 [HttpFoundation] Use the correct syntax for session gc based on Pdo driver (tanasecosminromeo) - * bug #25933 Disable CSP header on exception pages only in debug (ostrolucky) - * bug #25926 [Form] Fixed Button::setParent() when already submitted (HeahDude) - * bug #25927 [Form] Fixed submitting disabled buttons (HeahDude) - * bug #25397 [Console] Provide a DX where an array could be passed (Simperfit) - * bug #25858 [DI] Fix initialization of legacy containers by delaying include_once (nicolas-grekas) - * bug #25891 [DependencyInjection] allow null values for root nodes in YAML configs (xabbuh) - * bug #24864 Have weak_vendors ignore deprecations from outside (greg0ire) - * bug #25873 [Console] Fix using finally where the catch can also fail (nicolas-grekas) - * bug #25848 [Validator] add missing parent isset and add test (Simperfit) - * bug #25869 [Process] Skip environment variables with false value in Process (francoispluchino) - * bug #25864 [Yaml] don't split lines on carriage returns when dumping (xabbuh) - * bug #25863 [Yaml] trim spaces from unquoted scalar values (xabbuh) - * bug #25861 do not conflict with egulias/email-validator 2.0+ (xabbuh) - * bug #25851 [Validator] Conflict with egulias/email-validator 2.0 (emodric) - * bug #25837 [SecurityBundle] Don't register in memory users as services (chalasr) - * bug #25835 [HttpKernel] DebugHandlersListener should always replace the existing exception handler (nicolas-grekas) - * bug #25829 [Debug] Always decorate existing exception handlers to deal with fatal errors (nicolas-grekas) - * bug #25823 [Security] Notify that symfony/expression-language is not installed if ExpressionLanguage is used (giovannialbero1992) - * bug #25824 Fixing a bug where the dump() function depended on bundle ordering (weaverryan) - * bug #25763 [OptionsResolver] Fix options resolver with array allowed types (mcg-web) - * bug #25789 Enableable ArrayNodeDefinition is disabled for empty configuration (kejwmen) - * bug #25822 [Cache] Fix handling of apcu_fetch() edgy behavior (nicolas-grekas) - * bug #25816 Problem in phar see mergerequest #25579 (betzholz) - * bug #25781 [Form] Disallow transform dates beyond the year 9999 (curry684) - * bug #25287 [Serializer] DateTimeNormalizer handling of null and empty values (returning it instead of new object) (Simperfit) - * bug #25249 [Form] Avoid button label translation when it's set to false (TeLiXj) - * bug #25127 [TwigBridge] Pass the form-check-inline in parent (Simperfit) - * bug #25812 Copied NO language files to the new NB locale (derrabus) - * bug #25753 [Console] Fix restoring exception handler (nicolas-grekas, fancyweb) - * bug #25801 [Router] Skip anonymous classes when loading annotated routes (pierredup) - * bug #25508 [FrameworkBundle] Auto-enable CSRF if the component *+ session* are loaded (nicolas-grekas) - * bug #25657 [Security] Fix fatal error on non string username (chalasr) - * bug #25791 [Routing] Make sure we only build routes once (sroze) - * bug #25799 Fixed Request::__toString ignoring cookies (Toflar) - * bug #25755 [Debug] prevent infinite loop with faulty exception handlers (nicolas-grekas) - * bug #25771 [Validator] 19 digits VISA card numbers are valid (xabbuh) - * bug #25751 [FrameworkBundle] Add the missing `enabled` session attribute (sroze) - * bug #25750 [HttpKernel] Turn bad hosts into 400 instead of 500 (nicolas-grekas) - * bug #25699 [HttpKernel] Fix session handling: decouple "save" from setting response "private" (nicolas-grekas) - * bug #25490 [Serializer] Fixed throwing exception with option JSON_PARTIAL_OUTPUT_ON_ERROR (diversantvlz) - * bug #25737 [TwigBridge] swap filter/function and package names (xabbuh) - * bug #25731 [HttpFoundation] Always call proxied handler::destroy() in StrictSessionHandler (nicolas-grekas) - * bug #25733 [HttpKernel] Fix compile error when a legacy container is fresh again (nicolas-grekas) - * bug #25709 Tweaked some styles in the profiler tables (javiereguiluz) - * bug #25719 [HttpKernel] Uses cookies to track the requests redirection (sroze) - * bug #25696 [FrameworkBundle] Fix using "annotations.cached_reader" in after-removing passes (nicolas-grekas) - * feature #25669 [Security] Fail gracefully if the security token cannot be unserialized from the session (thewilkybarkid) - * bug #25700 Run simple-phpunit with --no-suggest option (ro0NL) - -* 4.0.3 (2018-01-05) - - * bug #25685 Use triggering file to determine weak vendors if when the test is run in a separate process (alexpott) - * bug #25671 Remove randomness from dumped containers (nicolas-grekas) - * bug #25532 [HttpKernel] Disable CSP header on exception pages (ostrolucky) - * bug #25678 [WebProfilerBundle] set the var in the right scope (Jochen Mandl) - * bug #25491 [Routing] Use the default host even if context is empty (sroze) - * bug #25672 [WebServerBundle] use interface_exists instead of class_exists (kbond) - * bug #25662 Dumper shouldn't use html format for phpdbg / cli-server (jhoff) - * bug #25529 [Validator] Fix access to root object when using composite constraint (ostrolucky) - * bug #25404 [Form] Remove group options without data on debug:form command (yceruto) - * bug #25430 Fixes for Oracle in PdoSessionHandler (elislenio) - * bug #25117 [FrameworkBundle] Make cache:clear "atomic" and consistent with cache:warmup (hkdobrev) - * bug #25583 [HttpKernel] Call Response->setPrivate() instead of sending raw header() when session is started (Toflar) - * bug #25601 [TwigBundle/Brige] catch missing requirements to throw meaningful exceptions (nicolas-grekas) - * bug #25547 [DX][DependencyInjection] Suggest to write an implementation if the interface cannot be autowired (sroze) - * bug #25599 Add application/ld+json format associated to json (vincentchalamon) - * bug #25623 [HttpFoundation] Fix false-positive ConflictingHeadersException (nicolas-grekas) - * bug #25624 [WebServerBundle] Fix escaping of php binary with arguments (nicolas-grekas) - * bug #25604 Add check for SecurityBundle in createAccessDeniedException (FGM) - * bug #25591 [HttpKernel] fix cleaning legacy containers (nicolas-grekas) - * bug #25526 [WebProfilerBundle] Fix panel break when stopwatch component is not installed. (umulmrum, javiereguiluz) - * bug #25606 Updating message to inform the user how to install the component (weaverryan) - * bug #25571 [SecurityBundle] allow auto_wire for SessionAuthenticationStrategy class (xavren) - * bug #25567 [Process] Fix setting empty env vars (nicolas-grekas) - * bug #25407 [Console] Commands with an alias should not be recognized as ambiguous (Simperfit) - * bug #25523 [WebServerBundle] fix a bug where require would not require the good file because of env (Simperfit) - * bug #25559 [Process] Dont use getenv(), it returns arrays and can introduce subtle breaks accros PHP versions (nicolas-grekas) - * bug #25552 [WebProfilerBundle] Let fetch() cast URL to string (ro0NL) - * bug #25521 [Console] fix a bug when you are passing a default value and passing -n would output the index (Simperfit) - -* 4.0.2 (2017-12-15) - - * bug #25489 [FrameworkBundle] remove esi/ssi renderers if inactive (dmaicher) - * bug #25502 Fixing wrong class_exists on interface (weaverryan) - * bug #25427 Preserve percent-encoding in URLs when performing redirects in the UrlMatcher (mpdude) - * bug #25480 [FrameworkBundle] add missing validation options to XSD file (xabbuh) - * bug #25487 [Console] Fix a bug when passing a letter that could be an alias (Simperfit) - * bug #25425 When available use AnnotationRegistry::registerUniqueLoader (jrjohnson) - * bug #25474 [DI] Optimize Container::get() for perf (nicolas-grekas) - * bug #24594 [Translation] Fix InvalidArgumentException when using untranslated plural forms from .po files (BjornTwachtmann) - * bug #25233 [TwigBridge][Form] Fix hidden currency element with Bootstrap 3 theme (julienfalque) - * bug #25413 [HttpKernel] detect deprecations thrown by container initialization during tests (nicolas-grekas) - * bug #25408 [Debug] Fix catching fatal errors in case of nested error handlers (nicolas-grekas) - * bug #25330 [HttpFoundation] Support 0 bit netmask in IPv6 (`::/0`) (stephank) - * bug #25378 [VarDumper] Fixed file links leave blank pages when ide is configured (antalaron) - * bug #25410 [HttpKernel] Fix logging of post-terminate errors/exceptions (nicolas-grekas) - * bug #25409 [Bridge/Doctrine] Drop "memcache" type (nicolas-grekas) - * bug #25417 [Process] Dont rely on putenv(), it fails on ZTS PHP (nicolas-grekas) - * bug #25333 [DI] Impossible to set an environment variable and then an array as container parameter (Phantas0s) - * bug #25447 [Process] remove false-positive BC breaking exception on Windows (nicolas-grekas) - * bug #25381 [DI] Add context to service-not-found exceptions thrown by service locators (nicolas-grekas) - * bug #25438 [Yaml] empty lines don't count for indent detection (xabbuh) - * bug #25412 Extend Argon2i support check to account for sodium_compat (mbabker) - * bug #25392 [HttpFoundation] Fixed default user-agent (3.X -> 4.X) (lyrixx) - * bug #25389 [Yaml] fix some edge cases with indented blocks (xabbuh) - * bug #25396 [Form] Fix debug:form command definition (yceruto) - * bug #25398 [HttpFoundation] don't prefix cookies with "Set-Cookie:" (pableu) - * bug #25354 [DI] Fix non-string class handling in PhpDumper (nicolas-grekas, sroze) - * bug #25340 [Serializer] Unset attributes when creating child context (dunglas) - * bug #25325 [Yaml] do not evaluate PHP constant names (xabbuh) - * bug #25380 [FrameworkBundle][Cache] register system cache clearer only if it's used (xabbuh) - * bug #25323 [ExpressionLanguage] throw an SyntaxError instead of an undefined index notice (Simperfit) - * bug #25363 [HttpKernel] Disable inlining on PHP 5 (nicolas-grekas) - * bug #25364 [DependencyInjection] Prevent a loop in aliases within the `findDefinition` method (sroze) - * bug #25337 Remove Exclusive Lock That Breaks NFS Caching (brianfreytag) - -* 4.0.1 (2017-12-05) - - * bug #25304 [Bridge/PhpUnit] Prefer $_SERVER['argv'] over $argv (ricknox) - * bug #25272 [SecurityBundle] fix setLogoutOnUserChange calls for context listeners (dmaicher) - * bug #25282 [DI] Register singly-implemented interfaces when doing PSR-4 discovery (nicolas-grekas) - * bug #25274 [Security] Adding a GuardAuthenticatorHandler alias (weaverryan) - * bug #25308 [FrameworkBundle] Fix a bug where a color tag will be shown when passing an antislash (Simperfit) - * bug #25278 Fix for missing whitespace control modifier in form layout (kubawerlos) - * bug #25306 [Form][TwigBridge] Fix collision between view properties and form fields (yceruto) - * bug #25305 [Form][TwigBridge] Fix collision between view properties and form fields (yceruto) - * bug #25236 [Form][TwigBridge] Fix collision between view properties and form fields (yceruto) - * bug #25312 [DI] Fix deep-inlining of non-shared refs (nicolas-grekas) - * bug #25309 [Yaml] parse newlines in quoted multiline strings (xabbuh) - * bug #25313 [DI] Fix missing unset leading to false-positive circular ref (nicolas-grekas) - * bug #25268 [DI] turn $private to protected in dumped container, to make cache:clear BC (nicolas-grekas) - * bug #25285 [DI] Throw an exception if Expression Language is not installed (sroze) - * bug #25241 [Yaml] do not eagerly filter comment lines (xabbuh) - * bug #25284 [DI] Cast ids to string, as done on 3.4 (nicolas-grekas, sroze) - * bug #25297 [Validator] Fixed the @Valid(groups={"group"}) against null exception case (vudaltsov) - * bug #25255 [Console][DI] Fail gracefully (nicolas-grekas) - * bug #25264 [DI] Trigger deprecation when setting a to-be-private synthetic service (nicolas-grekas) - * bug #25258 [link] Prevent warnings when running link with 2.7 (dunglas) - * bug #25244 [DI] Add missing deprecation when fetching private services from ContainerBuilder (nicolas-grekas) - * bug #24750 [Validator] ExpressionValidator should use OBJECT_TO_STRING (Simperfit) - * bug #25247 [DI] Fix false-positive circular exception (nicolas-grekas) - * bug #25226 [HttpKernel] Fix issue when resetting DumpDataCollector (Pierstoval) - * bug #25230 Use a more specific file for detecting the bridge (greg0ire) - * bug #25232 [WebProfilerBundle] [TwigBundle] Fix Profiler breaking XHTML pages (tistre) - -* 4.0.0 (2017-11-30) - - * bug #25220 [HttpFoundation] Add Session::isEmpty(), fix MockFileSessionStorage to behave like the native one (nicolas-grekas) - * bug #25209 [VarDumper] Dont use empty(), it chokes on eg GMP objects (nicolas-grekas) - * bug #25200 [HttpKernel] Arrays with scalar values passed to ESI fragment renderer throw deprecation notice (Simperfit) - * bug #25201 [HttpKernel] Add a better error messages when passing a private or non-tagged controller (Simperfit) - * bug #25155 [DependencyInjection] Detect case mismatch in autowiring (Simperfit, sroze) - * bug #25217 [Dotenv] Changed preg_match flags from null to 0 (deekthesqueak) - * bug #25180 [DI] Fix circular reference when using setters (nicolas-grekas) - * bug #25204 [DI] Clear service reference graph (nicolas-grekas) - * bug #25203 [DI] Fix infinite loop in InlineServiceDefinitionsPass (nicolas-grekas) - * bug #25185 [Serializer] Do not cache attributes if `attributes` in context (sroze) - * bug #25190 [HttpKernel] Keep legacy container files for concurrent requests (nicolas-grekas) - * bug #25182 [HttpFoundation] AutExpireFlashBag should not clear new flashes (Simperfit, sroze) - * bug #25174 [Translation] modify definitions only if the do exist (xabbuh) - * bug #25179 [FrameworkBundle][Serializer] Remove YamlEncoder definition if Yaml component isn't installed (ogizanagi) - * bug #25160 [DI] Prevent a ReflectionException during cache:clear when the parent class doesn't exist (dunglas) - * bug #25163 [DI] Fix tracking of env vars in exceptions (nicolas-grekas) - * bug #25162 [HttpKernel] Read $_ENV when checking SHELL_VERBOSITY (nicolas-grekas) - * bug #25158 [DI] Remove unreachable code (GawainLynch) - * bug #25152 [Form] Don't rely on `Symfony\Component\HttpFoundation\File\File` if http-foundation isn't in FileType (issei-m) - * bug #24987 [Console] Fix global console flag when used in chain (Simperfit) - * bug #25137 Adding checks for the expression language (weaverryan) - * bug #25151 [FrameworkBundle] Automatically enable the CSRF protection if CSRF manager exists (sroze) - * bug #25043 [Yaml] added ability for substitute aliases when mapping is on single line (Michał Strzelecki, xabbuh) - -* 4.0.0-RC2 (2017-11-24) - - * bug #25146 [DI] Dont resolve envs in service ids (nicolas-grekas) - * bug #25113 [Routing] Fix "config-file-relative" annotation loader resources (nicolas-grekas, sroze) - * bug #25065 [FrameworkBundle] Update translation commands to work with default paths (yceruto) - * bug #25109 Make debug:container search command case-insensitive (jzawadzki) - * bug #25121 [FrameworkBundle] Fix AssetsInstallCommand (nicolas-grekas) - * bug #25102 [Form] Fixed ContextErrorException in FileType (chihiro-adachi) - * bug #25130 [DI] Fix handling of inlined definitions by ContainerBuilder (nicolas-grekas) - * bug #25119 [DI] Fix infinite loop when analyzing references (nicolas-grekas) - * bug #25094 [FrameworkBundle][DX] Display a nice error message if an enabled component is missing (derrabus) - * bug #25100 [SecurityBundle] providerIds is undefined error when firewall provider is not specified (karser) - * bug #25100 [SecurityBundle] providerIds is undefined error when firewall provider is not specified (karser) - * bug #25100 [SecurityBundle] providerIds is undefined error when firewall provider is not specified (karser) - * bug #25097 [Bridge\PhpUnit] Turn "preserveGlobalState" to false by default, revert "Blacklist" removal (nicolas-grekas) - -* 4.0.0-RC1 (2017-11-21) - - * bug #25077 [Bridge/Twig] Let getFlashes starts the session (MatTheCat) - * bug #25082 [HttpKernel] Disable container inlining when legacy inlining has been used (nicolas-grekas) - * bug #25022 [Filesystem] Updated Filesystem::makePathRelative (inso) - * bug #25072 [Bridge/PhpUnit] Remove trailing "\n" from ClockMock::microtime(false) (joky) - * bug #25069 [Debug] Fix undefined variable $lightTrace (nicolas-grekas) - * bug #25053 [Serializer] Fixing PropertyNormalizer supports parent properties (Christopher Hertel) - * bug #25055 [DI] Analyze setter-circular deps more precisely (nicolas-grekas) - * feature #25056 [Bridge/PhpUnit] Sync the bridge version installed in vendor/ and in phpunit clone (nicolas-grekas) - * bug #25048 Allow EnumNode name to be null (MatTheCat) - * bug #25045 [SecurityBundle] Don't trigger auto-picking notice if provider is set per listener (chalasr) - * bug #25033 [FrameworkBundle] Dont create empty bundles directory by default (ro0NL) - * bug #25037 [DI] Skip hot_path tag for deprecated services as their class might also be (nicolas-grekas) - * bug #25038 [Cache] Memcached options should ignore "lazy" (nicolas-grekas) - * bug #25014 Move deprecation under use statements (greg0ire) - * bug #25030 [Console] Fix ability to disable lazy commands (chalasr) - * bug #25032 [Bridge\PhpUnit] Disable broken auto-require mechanism of phpunit (nicolas-grekas) - * bug #25016 [HttpKernel] add type-hint for the requestType (Simperfit) - * bug #25027 [FrameworkBundle] Hide server:log command based on deps (sroze) - * bug #24991 [DependencyInjection] Single typed argument can be applied on multiple parameters (nicolas-grekas, sroze) - * bug #24983 [Validator] enter the context in which to validate (xabbuh) - * bug #24956 Fix ambiguous pattern (weltling) - * bug #24732 [DependencyInjection] Prevent service:method factory notation in PHP config (vudaltsov) - * bug #24979 [HttpKernel] remove services resetter even when it's an alias (xabbuh) - * bug #24972 [HttpKernel] Fix service arg resolver for controllers as array callables (sroze, nicolas-grekas) - * bug #24971 [FrameworkBundle] Empty event dispatcher earlier in CacheClearCommand (nicolas-grekas) - * security #24995 Validate redirect targets using the session cookie domain (nicolas-grekas) - * security #24994 Prevent bundle readers from breaking out of paths (xabbuh) - * security #24993 Ensure that submitted data are uploaded files (xabbuh) - * security #24992 Namespace generated CSRF tokens depending of the current scheme (dunglas) - * bug #24975 [DomCrawler] Type fix Crawler:: discoverNamespace() (VolCh) - * bug #24954 [DI] Fix dumping with custom base class (nicolas-grekas) - * bug #24952 [HttpFoundation] Fix session-related BC break (nicolas-grekas, sroze) - * bug #24943 [FrameworkBundle] Wire the translation.reader service instead of deprecated translation.loader in commands (ogizanagi) - -* 4.0.0-BETA4 (2017-11-12) - - * bug #24874 [TwigBridge] Fixed the .form-check-input class in the bs4 templates (vudaltsov) - * bug #24929 [Console] Fix traversable autocomplete values (ro0NL) - * feature #24860 [FrameworkBundle] Add default translations path option and convention (yceruto) - * bug #24921 [Debug] Remove false-positive deprecation from DebugClassLoader (nicolas-grekas) - * bug #24856 [FrameworkBundle] Add default mapping path for validator component in bundle-less app (yceruto) - * bug #24833 [FrameworkBundle] Add default mapping path for serializer component in bundle-less app (yceruto) - * bug #24908 [WebServerBundle] Prevent console.terminate from being fired when server:start finishes (kbond) - * bug #24888 [FrameworkBundle] Specifically inject the debug dispatcher in the collector (ogizanagi) - * bug #24909 [Intl] Update ICU data to 60.1 (jakzal) - * bug #24870 [YAML] Allow to parse custom tags when linting yaml files (pierredup) - * bug #24910 [HttpKernel][Debug] Remove noise from stack frames of deprecations (nicolas-grekas) - * bug #24906 [Bridge/ProxyManager] Remove direct reference to value holder property (nicolas-grekas) - * feature #24887 [Cache][Lock] Add RedisProxy for lazy Redis connections (nicolas-grekas) - * bug #24633 [Config] Fix cannotBeEmpty() (ro0NL) - * bug #24900 [Validator] Fix Costa Rica IBAN format (Bozhidar Hristov) - * bug #24904 [Validator] Add Belarus IBAN format (Bozhidar Hristov) - * bug #24837 [TwigBridge] [Bootstrap 4] Fix validation error design for expanded choiceType (ostrolucky) - * bug #24878 [HttpFoundation] Prevent PHP from sending Last-Modified on session start (nicolas-grekas) - * bug #24881 [WebserverBundle] fixed the bug that caused that the webserver would … (Serkan Yildiz) - * bug #24722 Replace more docblocks by type-hints (nicolas-grekas) - * bug #24850 [DI] Fix cannot bind env var (ogizanagi) - * bug #24851 [TwigBridge] Fix BC break due required twig environment (ro0NL) - -* 4.0.0-BETA3 (2017-11-05) - - * bug #24531 [HttpFoundation] Fix forward-compat of NativeSessionStorage with PHP 7.2 (sroze) - * bug #24828 [DI] Fix the "almost-circular refs" fix (nicolas-grekas) - * bug #24665 Fix dump panel hidden when closing a dump (julienfalque) - * bug #24802 [TwigBridge] [Bootstrap 4] Fix hidden errors (ostrolucky) - * bug #24816 [Serializer] Fix extra attributes when no group specified (ogizanagi) - * bug #24822 [DI] Fix "almost-circular" dependencies handling (nicolas-grekas) - * bug #24821 symfony/form auto-enables symfony/validator, even when not present (weaverryan) - * bug #24824 [FrameworkBundle][Config] fix: do not add resource checkers for no-debug (dmaicher) - * bug #24814 [Intl] Make intl-data tests pass and save language aliases again (jakzal) - * bug #24810 [Serializer] readd default argument value (xabbuh) - * bug #24809 [Config] Fix dump of config references for deprecated nodes (chalasr) - * bug #24796 [PhpUnitBridge] Fixed fatal error in CoverageListener when something goes wrong in Test::setUpBeforeClass (lyrixx) - * bug #24774 [HttpKernel] Let the storage manage the session starts (sroze) - * bug #24735 [VarDumper] fix trailling comma when dumping an exception (Simperfit) - * bug #24770 [Validator] Fix TraceableValidator is reset on data collector instantiation (ogizanagi) - * bug #24764 [HttpFoundation] add Early Hints to Reponse to fix test (Simperfit) - * bug #24759 Removes \n or space when $context/$extra are empty (kirkmadera) - * bug #24758 Throwing exception if redis and predis unavailable (aequasi) - -* 4.0.0-BETA2 (2017-10-30) - - * bug #24728 [Bridge\Twig] fix bootstrap checkbox_row to render properly & remove spaceless (arkste) - * bug #24709 [HttpKernel] Move services reset to Kernel::handle()+boot() (nicolas-grekas) - * bug #24703 [TwigBridge] Bootstrap 4 form theme fixes (vudaltsov) - * bug #24744 debug:container --types: Fix bug with non-existent classes (weaverryan) - * bug #24747 [VarDumper] HtmlDumper: fix collapsing nodes with depth < maxDepth (ogizanagi) - * bug #24743 [FrameworkBundle] Do not activate the cache if Doctrine's cache is not present (sroze) - * bug #24605 [FrameworkBundle] Do not load property_access.xml if the component isn't installed (ogizanagi) - * bug #24710 [TwigBridge] Fix template paths in profiler (ro0NL) - * bug #24706 [DependencyInjection] Add the possibility to disable assets via xml (renatomefi) - * bug #24696 Ensure DeprecationErrorHandler::collectDeprecations() is triggered (alexpott) - * bug #24711 [TwigBridge] Re-add Bootstrap 3 Checkbox Layout (arkste) - * bug #24713 [FrameworkBundle] fix CachePoolPrunerPass to use correct command service id (kbond) - * bug #24686 Fix $_ENV/$_SERVER precedence in test framework (fabpot) - * bug #24691 [HttpFoundation] Fix caching of session-enabled pages (nicolas-grekas) - * feature #24677 [HttpFoundation] Allow DateTimeImmutable in Response setters (derrabus) - * bug #24694 [Intl] Allow passing null as a locale fallback (jakzal) - * bug #24606 [HttpFoundation] Fix FileBag issue with associative arrays (enumag) - * bug #24673 [DI] Throw when a service name or an alias contains dynamic values (prevent an infinite loop) (dunglas) - * bug #24684 [Security] remove invalid deprecation notice on AbstractGuardAuthenticator::supports() (kbond) - * bug #24681 Fix isolated error handling (alexpott) - * bug #24575 Ensure that PHPUnit's error handler is still working in isolated tests (alexpott) - * bug #24597 [PhpUnitBridge] fix deprecation triggering test detection (xabbuh) - * feature #24671 [DI] Handle container.autowiring.strict_mode to opt-out from legacy autowiring (nicolas-grekas) - * bug #24660 Escape trailing \ in QuestionHelper autocompletion (kamazee) - * bug #24624 [Security] Fix missing BC layer for AbstractGuardAuthenticator::getCredentials() (chalasr) - * bug #24598 Prefer line formatter on missing cli dumper (greg0ire) - * bug #24635 [DI] Register default env var provided types (ro0NL) - * bug #24620 [FrameworkBundle][Workflow] Fix deprectation when checking workflow.registry service in dump command (Jean-Beru) - * bug #24644 [Security] Fixed auth provider authenticate() cannot return void (glye) - * bug #24642 [Routing] Fix resource miss (dunglas) - * bug #24608 Adding the Form default theme files to be warmed up in Twig's cache (weaverryan) - * bug #24626 streamed response should return $this (DQNEO) - * feature #24631 [Form] Fix FormEvents::* constant and value matching (yceruto, javiereguiluz) - * bug #24630 [WebServerBundle] Prevent commands from being registered by convention (chalasr) - * bug #24589 Username and password in basic auth are allowed to contain '.' (Richard Quadling) - * bug #24566 Fixed unsetting from loosely equal keys OrderedHashMap (maryo) - * bug #24570 [Debug] Fix same vendor detection in class loader (Jean-Beru) - * bug #24573 Fixed pathinfo calculation for requests starting with a question mark. (syzygymsu) - * bug #24565 [Serializer] YamlEncoder: throw if the Yaml component isn't installed (dunglas) - * bug #24563 [Serializer] ObjectNormalizer: throw if PropertyAccess isn't installed (dunglas) - * bug #24571 [PropertyInfo] Add support for the iterable type (dunglas) - * bug #24579 pdo session fix (mxp100) - * bug #24536 [Security] Reject remember-me token if UserCheckerInterface::checkPostAuth() fails (kbond) - -* 4.0.0-BETA1 (2017-10-19) - - * feature #24583 Adding a new debug:autowiring command (weaverryan) - * feature #24523 [HttpFoundation] Make sessions secure and lazy (nicolas-grekas) - * feature #22610 [Form] [TwigBridge] Added option to disable usage of default themes when rendering a form (emodric) - * feature #23112 [OptionsResolver] Support array of types in allowed type (pierredup) - * feature #24321 added ability to handle parent classes for PropertyNormalizer (ivoba) - * feature #24505 [HttpKernel] implement reset() in DumpDataCollector (xabbuh) - * feature #24425 [Console][HttpKernel] Handle new SHELL_VERBOSITY env var, also configures the default logger (nicolas-grekas) - * feature #24503 [MonologBridge][EventDispatcher][HttpKernel] remove deprecated features (xabbuh) - * feature #24387 [FORM] Prevent forms from extending itself as a parent (pierredup) - * feature #24484 [DI] Throw accurate failures when accessing removed services (nicolas-grekas) - * feature #24208 [Form] Display option definition from a given form type (yceruto, ogizanagi) - * feature #22228 [HttpKernel] minor: add ability to construct with headers on http exceptions (gsdevme) - * feature #24467 [Process] drop non-existent working directory support (xabbuh) - * feature #23499 [Workflow] add guard is_valid() method support (alain-flaus, lyrixx) - * feature #24364 [DependencyInjection][Filesystem][Security][SecurityBundle] remove deprecated features (xabbuh) - * feature #24441 [Bridge\Doctrine][FrameworkBundle] Remove legacy uses of ContainerAwareInterface (nicolas-grekas) - * feature #24388 [Security] Look at headers for switch_user username (chalasr) - * feature #24447 [Session] remove deprecated session handlers (Tobion) - * feature #24446 [Security] Remove GuardAuthenticatorInterface (chalasr) - * feature #23708 Added deprecation to cwd not existing Fixes #18249 (alexbowers) - * feature #24443 [Session] deprecate MemcacheSessionHandler (Tobion) - * feature #24409 [Bridge\Doctrine][FrameworkBundle] Deprecate some remaining uses of ContainerAwareTrait (nicolas-grekas) - * feature #24438 [Session][VarDumper] Deprecate accepting legacy mongo extension (Tobion) - * feature #24389 [DoctrineBridge] Deprecate dbal session handler (Tobion) - * feature #16835 [Security] Add Guard authenticator method (Amo, chalasr) - * feature #24289 [FrameworkBundle][HttpKernel] Reset profiler (derrabus) - * feature #24144 [FrameworkBundle] Expose dotenv in bin/console about (ro0NL) - * feature #24403 [FrameworkBundle][Routing] Show welcome message if no routes are configured (yceruto) - * feature #24398 [DI] Remove AutowireExceptionPass (ogizanagi) - * feature #22679 [Form] Add tel and color types (apetitpa) - * feature #23845 [Validator] Add unique entity violation cause (Ilya Vertakov) - * feature #22132 [Lock] Automaticaly release lock when user forget it (jderusse) - * feature #21751 Bootstrap4 support for Twig form theme (hiddewie, javiereguiluz) - * feature #24383 [FrameworkBundle] Don't clear app pools on cache:clear (nicolas-grekas) - * feature #24148 [Form] Hide label button when its setted to false (TeLiXj) - * feature #24380 [SecurityBundle] Remove auto picking the first provider (ogizanagi) - * feature #24378 [SecurityBundle] Deprecate auto picking the first provider (ogizanagi) - * feature #24260 [Security] Add impersonation support for stateless authentication (chalasr) - * feature #24300 [HttpKernel][FrameworkBundle] Add a minimalist default PSR-3 logger (dunglas) - * feature #21604 [Security] Argon2i Password Encoder (zanbaldwin) - * feature #24372 [DowCrawler] Default to UTF-8 when possible (nicolas-grekas) - * feature #24264 [TwigBundle] Improve the overriding of bundle templates (yceruto) - * feature #24197 [Translation] Moved PhpExtractor and PhpStringTokenParser to Translation component (Nyholm) - * feature #24362 [HttpKernel] Deprecate some compiler passes in favor of tagged iterator args (nicolas-grekas) - * feature #21027 [Asset] Provide default context (ro0NL) - * feature #22200 [DI] Reference tagged services in config (ro0NL) - * feature #24337 Adding a shortcuts for the main security functionality (weaverryan, javiereguiluz) - * feature #24358 [TwigBundle] register an identity translator as fallback (xabbuh) - * feature #24357 [Yaml] include file and line no in deprecation message (xabbuh) - * feature #24330 [FrameworkBundle] register class metadata factory alias (xabbuh) - * feature #24348 [SecurityBundle] Remove remaining ACL stuff from SecurityBundle (ogizanagi) - * feature #24349 [SecurityBundle] Add missing AclSchemaListener deprecation (ogizanagi) - * feature #24202 [Filesystem] deprecate relative paths in makePathRelative() (xabbuh) - * feature #21716 [Serializer] Add Support for `object_to_populate` in CustomNormalizer (chrisguitarguy) - * feature #21960 Remove Validator\TypeTestCase and add validator logic to base TypeTestCase (pierredup) - * feature #24338 [HttpFoundation] Removed compatibility layer for PHP <5.4 sessions (afurculita) - * feature #22113 [Lock] Include lock component in framework bundle (jderusse) - * feature #24236 [WebProfilerBundle] Render file links for twig templates (ro0NL) - * feature #21239 [Serializer] throw more specific exceptions (xabbuh) - * feature #24341 [SecurityBundle] Remove ACL related code (chalasr) - * feature #24256 CsvEncoder handling variable structures and custom header order (Oliver Hoff) - * feature #23471 [Finder] Add a method to check if any results were found (duncan3dc) - * feature #23149 [PhpUnitBridge] Added a CoverageListener to enhance the code coverage report (lyrixx) - * feature #24161 [HttpKernel] Remove bundle inheritance (fabpot) - * feature #24318 [SecurityBundle] Deprecate ACL related code (chalasr) - * feature #24335 [Security][SecurityBundle] Deprecate the HTTP digest auth (ogizanagi) - * feature #21951 [Security][Firewall] Passing the newly generated security token to the event during user switching (klandaika) - * feature #23485 [Config] extracted the xml parsing from XmlUtils::loadFile into XmlUtils::parse (Basster) - * feature #22890 [HttpKernel] Add ability to configure catching exceptions for Client (kbond) - * feature #24239 [HttpFoundation] Deprecate compatibility with PHP <5.4 sessions (afurculita) - * feature #23882 [Security] Deprecated not being logged out after user change (iltar) - * feature #24200 Added an alias for FlashBagInterface in config (tifabien) - * feature #24295 [DI][DX] Throw exception on some ContainerBuilder methods used from extensions (ogizanagi) - * feature #24253 [Yaml] support parsing files (xabbuh) - * feature #24290 Adding Definition::addError() and a compiler pass to throw errors as exceptions (weaverryan) - * feature #24301 [DI] Add AutowireRequiredMethodsPass to fix bindings for `@required` methods (nicolas-grekas) - * feature #24226 [Cache] Add ResettableInterface to allow resetting any pool's local state (nicolas-grekas) - * feature #24303 [FrameworkBundle] allow forms without translations and validator (xabbuh) - * feature #24291 [SecurityBundle] Reset the authentication token between requests (derrabus) - * feature #24280 [VarDumper] Make `dump()` a little bit more easier to use (freekmurze) - * feature #24277 [Serializer] Getter for extra attributes in ExtraAttributesException (mdeboer) - * feature #24257 [HttpKernel][DI] Enable Kernel to implement CompilerPassInterface (nicolas-grekas) - * feature #23834 [DI] Add "PHP fluent format" for configuring the container (nicolas-grekas) - * feature #24180 [Routing] Add PHP fluent DSL for configuring routes (nicolas-grekas) - * feature #24232 [Bridge\Doctrine] Add "DoctrineType::reset()" method (nicolas-grekas) - * feature #24238 [DI] Turn services and aliases private by default, with BC layer (nicolas-grekas) - * feature #24242 [Form] Remove deprecated ChoiceLoaderInterface implementation in TimezoneType (ogizanagi) - * feature #23648 [Form] Add input + regions options to TimezoneType (ro0NL) - * feature #24185 [Form] Display general forms information on debug:form (yceruto) - * feature #23747 [Serializer][FrameworkBundle] Add a DateInterval normalizer (Lctrs) - * feature #24193 [FrameworkBundle] Reset stopwatch between requests (derrabus) - * feature #24160 [HttpKernel] Deprecate bundle inheritance (fabpot) - * feature #24159 Remove the profiler.matcher configuration (fabpot) - * feature #24155 [FrameworkBundle][HttpKernel] Add DI tag for resettable services (derrabus) - * feature #23625 Feature #23583 Add current and fallback locales in WDT / Profiler (nemoneph) - * feature #24179 [TwigBundle] Add default templates directory and option to configure it (yceruto) - * feature #24176 [Translation] drop MessageSelector support in the Translator (xabbuh) - * feature #24104 Make as many services private as possible (nicolas-grekas) - * feature #18314 [Translation] added support for adding custom message formatter (aitboudad) - * feature #24158 deprecated profiler.matcher configuration (fabpot) - * feature #23728 [WebProfilerBundle] Removed the deprecated web_profiler.position option (javiereguiluz) - * feature #24131 [Console] Do not display short exception trace for common console exceptions (yceruto) - * feature #24080 Deprecated the web_profiler.position option (javiereguiluz) - * feature #24114 [SecurityBundle] Throw a meaningful exception when an undefined user provider is used inside a firewall (chalasr) - * feature #24122 [DI] rename ResolveDefinitionTemplatesPass to ResolveChildDefinitionsPass (nicolas-grekas) - * feature #23901 [DI] Allow processing env vars (nicolas-grekas) - * feature #24093 [FrameworkBundle] be able to enable workflow support explicitly (xabbuh) - * feature #24064 [TwigBridge] Show Twig's loader paths on debug:twig command (yceruto) - * feature #23978 [Cache] Use options from Memcached DSN (Bukashk0zzz) - * feature #24067 [HttpKernel] Dont register env parameter resource (ro0NL) - * feature #24075 Implemented PruneableInterface on TagAwareAdapter (Toflar) - * feature #23262 Add scalar typehints/return types (chalasr, xabbuh) - * feature #21414 [Console] Display file and line on Exception (arno14) - * feature #24068 [HttpKernel] Deprecate EnvParametersResource (ro0NL) - * feature #22542 [Lock] Check TTL expiration in lock acquisition (jderusse) - * feature #24031 [Routing] Add the possibility to define a prefix for all routes of a controller (fabpot) - * feature #24052 [DI] Remove case insensitive parameter names (ro0NL) - * feature #23967 [VarDumper] add force-collapse/expand + use it for traces (nicolas-grekas) - * feature #24033 [DI] Add ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE (nicolas-grekas) - * feature #24026 [Security] add impersonator_user to "User was reloaded" log message (gharlan) - * feature #24014 [Translator] Remove deprecated feature (maidmaid) - * feature #23603 [Cache] Add (pdo|chain) cache (adapter|simple) prune method (robfrawley) - * feature #23694 [Form] Add debug:form command (yceruto) - * feature #24028 [Yaml] mark some classes as final (xabbuh) - * feature #22543 [Lock] Expose an expiringDate and isExpired method in Lock (jderusse) - * feature #23667 [Translation] Create an TranslationReaderInterface and move TranslationLoader to TranslationComponent (Nyholm) - * feature #24024 [config] Add ability to deprecate a node (sanpii) - * feature #23668 [VarDumper] Add period caster (maidmaid) - * feature #23991 [DI] Improve psr4-based service discovery (alternative implementation) (kbond) - * feature #22382 [config] Add abbitily to deprecate a node (Nyholm, fabpot, sanpii) - * feature #23947 [Translation] Adding the ability do load in xliff2.0 (Nyholm) - * feature #23887 [Console] Allow commands to provide a default name for compile time registration (chalasr, nicolas-grekas) - * feature #23874 [DI] Case sensitive parameter names (ro0NL) - * feature #23936 Remove some sf2 references (fabpot) - * feature #23680 [Webprofiler] Added blocks that allows extension of the profiler page. (Nyholm) - * feature #23665 Create an interface for TranslationWriter (Nyholm) - * feature #23890 [Translation] Adding the ability do dump in xliff2.0 (Nyholm) - * feature #23862 [SecurityBundle] resolve class name parameter inside AddSecurityVotersPass (pjarmalavicius) - * feature #23915 [DI] Allow get available services from service locator (Koc) - * feature #23792 [HttpKernel][FrameworkBundle] Add RebootableInterface, fix and un-deprecate cache:clear with warmup (nicolas-grekas) - * feature #23227 Add support for "controller" keyword for configuring routes controllers (voronkovich) - * feature #23815 [FrameworkBundle][SecurityBundle][TwigBundle][Yaml] remove deprecated code (xabbuh) - * feature #23857 [HttpKernel] Remove convention based commands registration (chalasr) - * feature #23869 [Console] Made console command shortcuts case insensitive (thanosp) - * feature #23855 [DI] Allow dumping inline services in Yaml (nicolas-grekas) - * feature #23836 [FrameworkBundle] Catch Fatal errors in commands registration (chalasr) - * feature #23805 [HttpKernel] Deprecated commands auto-registration (GuilhemN) - * feature #23816 [Debug] Detect internal and deprecated methods (GuilhemN) - * feature #23812 [FrameworkBundle] Allow micro kernel to subscribe events easily (ogizanagi) - * feature #22187 [DependencyInjection] Support local binding (GuilhemN) - * feature #23741 [DI] Generate one file per service factory (nicolas-grekas) - * feature #23807 [Debug] Trigger a deprecation when using an internal class/trait/interface (GuilhemN) - * feature #22587 [Workflow] Add transition completed event (izzyp) - * feature #23624 [FrameworkBundle] Commands as a service (ro0NL) - * feature #21111 [Validator] add groups support to the Valid constraint (xabbuh) - * feature #20361 [Config] Enable cannotBeEmpty along with requiresAtLeastOneElement (ro0NL) - * feature #23790 [Yaml] remove legacy php/const and php/object tag support (xabbuh) - * feature #23754 [Lock] Remove Filesystem\LockHandler (jderusse) - * feature #23712 [DependencyInjection] Deprecate autowiring service auto-registration (GuilhemN) - * feature #23719 Autoconfigure instances of ArgumentValueResolverInterface (BPScott) - * feature #23706 [Webprofiler] Improve sql explain table display (mimol91) - * feature #23709 [VarDumper] Make dump() variadic (chalasr) - * feature #23724 [Lock] Deprecate Filesystem/LockHandler (jderusse) - * feature #23593 [Workflow] Adding workflow name to the announce event (Nyholm) - * feature #20496 [Form] Allow pass filter callback to delete_empty option. (Koc) - * feature #23451 [Cache] Add (filesystem|phpfiles) cache (adapter|simple) prune method and prune command (robfrawley) - * feature #23519 [TwigBundle] Commands as a service (ro0NL) - * feature #23591 [VarDumper] Add time zone caster (maidmaid) - * feature #23614 [VarDumper] Remove low PHP version and hhvm compat in interval caster (maidmaid) - * feature #22317 [Console] Make SymfonyQuestionHelper::ask optional by default (ro0NL) - * feature #23510 [Console] Add a factory command loader for standalone application with lazy-loading needs (ogizanagi) - * feature #23357 [VarDumper] Add interval caster (maidmaid) - * feature #23550 [DebugBundle] Added min_depth to Configuration (james-johnston-thumbtack) - * feature #23561 [DI] Optimize use of private and pre-defined services (nicolas-grekas) - * feature #23569 Remove last legacy codes (nicolas-grekas) - * feature #23570 [FrameworkBundle] Make RouterCacheWarmer implement ServiceSubscriberInterface (nicolas-grekas) - * feature #22783 [TwigBridge] remove deprecated features (xabbuh) - * feature #23437 [TwigBridge] deprecate TwigRenderer (Tobion) - * feature #22801 [DI] Removed deprecated setting private/pre-defined services (ro0NL) - * feature #23515 [VarDumper] Added setMinDepth to VarCloner (james-johnston-thumbtack) - * feature #23484 [DI] Remove remaining deprecated features (nicolas-grekas) - * feature #23404 [Serializer] AbstractObjectNormalizer: Allow to disable type enforcement (ogizanagi) - * feature #23380 [Process] Remove enhanced sigchild compatibility (maidmaid) - * feature #21086 [MonologBridge] Add TokenProcessor (maidmaid) - * feature #22576 [Validator] Allow to use a property path to get value to compare in comparison constraints (ogizanagi) - * feature #22689 [DoctrineBridge] Add support for doctrin/dbal v2.6 types (jvasseur) - * feature #22734 [Console] Add support for command lazy-loading (chalasr) - * feature #19034 [Security] make it possible to configure a custom access decision manager service (xabbuh) - * feature #23037 [TwigBundle] Added a RuntimeExtensionInterface to take advantage of autoconfigure (lyrixx) - * feature #22811 [DI] Remove deprecated case insensitive service ids (ro0NL) - * feature #22176 [DI] Allow imports in string format for YAML (ro0NL) - * feature #23295 [Security] Lazy load user providers (chalasr) - * feature #23440 [Routing] Add matched and default parameters to redirect responses (artursvonda, Tobion) - * feature #22804 [Debug] Removed ContextErrorException (mbabker) - * feature #22762 [Yaml] Support tagged scalars (GuilhemN) - * feature #22832 [Debug] Deprecate support for stacked errors (mbabker) - * feature #21469 [HttpFoundation] Find the original request protocol version (thewilkybarkid) - * feature #23431 [Validator] Add min/max amount of pixels to Image constraint (akeeman) - * feature #23223 Add support for microseconds in Stopwatch (javiereguiluz) - * feature #22341 [BrowserKit] Emulate back/forward browser navigation (e-moe) - * feature #22619 [FrameworkBundle][Translation] Move translation compiler pass (lepiaf) - * feature #22620 [FrameworkBundle][HttpKernel] Move httpkernel pass (lepiaf) - * feature #23402 [Ldap] Remove the RenameEntryInterface interface (maidmaid) - * feature #23337 [Component][Serializer][Normalizer] : Deal it with Has Method for the Normalizer/Denormalizer (jordscream) - * feature #23391 [Validator] Remove support of boolean value for the checkDNS option (maidmaid) - * feature #23376 [Process] Remove enhanced Windows compatibility (maidmaid) - * feature #22588 [VarDumper] Add filter in VarDumperTestTrait (maidmaid) - * feature #23288 [Yaml] deprecate the !str tag (xabbuh) - * feature #23039 [Validator] Support for parsing PHP constants in yaml loader (mimol91) - * feature #22431 [VarDumper] Add date caster (maidmaid) - * feature #23285 [Stopwatch] Add a reset method (jmgq) - * feature #23320 [WebServer] Allow * to bind all interfaces (as INADDR_ANY) (jpauli, fabpot) - * feature #23272 [FrameworkBundle] disable unusable fragment renderers (xabbuh) - * feature #23332 [Yaml] fix the displayed line number (fabpot, xabbuh) - * feature #23324 [Security] remove support for defining voters that don't implement VoterInterface. (hhamon) - * feature #23294 [Yaml][Lint] Add line numbers to JSON output. (WybrenKoelmans) - * feature #22836 [Process] remove deprecated features (xabbuh) - * feature #23286 [Yaml] remove deprecated unspecific tag behavior (xabbuh) - * feature #23026 [SecurityBundle] Add user impersonation info and exit action to the profiler (yceruto) - * feature #22863 [HttpFoundation] remove deprecated features (xabbuh) - * feature #22932 [HttpFoundation] Adds support for the immutable directive in the cache-control header (twoleds) - * feature #22554 [Profiler][Validator] Add a validator panel in profiler (ogizanagi) - * feature #22124 Shift responsibility for keeping Date header to ResponseHeaderBag (mpdude) - * feature #23122 Xml encoder optional type cast (ragboyjr) - * feature #23207 [FrameworkBundle] Allow .yaml file extension everywhere (ogizanagi) - * feature #23076 [Validator] Adds support to check specific DNS record type for URL (iisisrael) - * feature #22629 [Security] Trigger a deprecation when a voter is missing the VoterInterface (iltar) - * feature #22636 [Routing] Expose request in route conditions, if needed and possible (ro0NL) - * feature #22909 [Yaml] Deprecate using the non-specific tag (GuilhemN) - * feature #23184 [HttpFoundation] Remove obsolete ini settings for sessions (fabpot) - * feature #23042 Consistent error handling in remember me services (lstrojny) - * feature #22444 [Serializer] DateTimeNormalizer: allow to provide timezone (ogizanagi) - * feature #23143 [DI] Reference instead of inline for array-params (nicolas-grekas) - * feature #23154 [WebProfilerBundle] Sticky ajax window (ro0NL) - * feature #23161 [FrameworkBundle] Deprecate useless --no-prefix option (chalasr) - * feature #23169 [FrameworkBundle] Remove KernelTestCase deprecated code (ogizanagi) - * feature #23105 [SecurityBundle][Profiler] Give info about called security listeners in profiler (chalasr) - * feature #23148 [FrameworkBundle] drop hard dependency on the Stopwatch component (xabbuh) - * feature #23131 [FrameworkBundle] Remove dependency on Doctrine cache (fabpot) - * feature #23114 [SecurityBundle] Lazy load security listeners (chalasr) - * feature #23111 [Process] Deprecate ProcessBuilder (nicolas-grekas) - * feature #22675 [FrameworkBundle] KernelTestCase: deprecate not using KERNEL_CLASS (ogizanagi) - * feature #22917 [VarDumper] Cycle prev/next searching in HTML dumps (ro0NL) - * feature #23044 Automatically enable the routing annotation loader (GuilhemN) - * feature #22696 [PropertyInfo] Made ReflectionExtractor's prefix lists instance variables (neemzy) - * feature #23056 [WebProfilerBundle] Remove WebProfilerExtension::dumpValue() (ogizanagi) - * feature #23035 Deprecate passing a concrete service in optional cache warmers (romainneutron) - * feature #23036 Implement ServiceSubscriberInterface in optional cache warmers (romainneutron) - * feature #23046 [Form] Missing deprecated paths removal (ogizanagi) - * feature #23022 [Di] Remove closure-proxy arguments (nicolas-grekas) - * feature #22758 Remove HHVM support (fabpot) - * feature #22743 [Serializer] Remove support for deprecated signatures (dunglas) - * feature #22907 [Serializer] remove remaining deprecated features (xabbuh) - * feature #22886 [HttpKernel] remove deprecated features (xabbuh) - * feature #22906 [Console] remove remaining deprecated features (xabbuh) - * feature #22879 [Translation] remove deprecated features (xabbuh) - * feature #22880 [Routing] remove deprecated features (xabbuh) - * feature #22903 [DI] Deprecate XML services without ID (ro0NL) - * feature #22770 [Yaml] remove deprecated features (xabbuh) - * feature #22785 [ProxyManagerBridge] remove deprecated features (xabbuh) - * feature #22800 [FrameworkBundle] Remove deprecated code (ogizanagi) - * feature #22597 [Lock] Re-add the Lock component in 3.4 (jderusse) - * feature #22860 [Form] remove deprecated features (xabbuh) - * feature #22803 [DI] Deprecate Container::initialized() for privates (ro0NL) - * feature #22773 [DependencyInjection] remove deprecated autowiring_types feature (hhamon) - * feature #22809 [DI] Remove deprecated generating a dumped container without populating the method map (ro0NL) - * feature #22764 [DI] Remove deprecated dumping an uncompiled container (ro0NL) - * feature #22820 Remove PHP < 7.1.3 code (ogizanagi) - * feature #22763 [DI] Remove deprecated isFrozen() (ro0NL) - * feature #22837 [VarDumper] remove deprecated features (xabbuh) - * feature #22777 [Console] Remove deprecated console.exception event (mbabker) - * feature #22792 [PhpUnitBridge] remove deprecated features (xabbuh) - * feature #22821 [Security] remove deprecated features (xabbuh) - * feature #22750 [DependencyInjection] remove deprecated code in YamlFileLoader class (hhamon) - * feature #22828 [Finder] Deprecate FilterIterator (ogizanagi) - * feature #22791 [MonologBridge] remove deprecated features (xabbuh) - * feature #22740 [SecurityBundle][Security][Finder] Remove deprecated code paths (ogizanagi) - * feature #22823 [PropertyAccess] remove deprecated features (xabbuh) - * feature #22826 [Validator] improve strict option value deprecation (xabbuh) - * feature #22799 [Console] Remove deprecated features (chalasr) - * feature #22786 [ClassLoader][HttpKernel] Remove ClassLoader component & Kernel::loadClassCache (ogizanagi, xabbuh) - * feature #22795 [Validator] remove deprecated features (xabbuh) - * feature #22784 [DoctrineBridge] remove deprecated features (xabbuh) - * feature #22749 Remove deprecated container injections and compiler passes (chalasr) - * feature #22796 [EventDispatcher] remove deprecated features (xabbuh) - * feature #22797 [Ldap] remove deprecated features (xabbuh) - * feature #22794 [ExpressionLanguage] remove deprecated features (xabbuh) - * feature #22779 [BC Break] Removed BC layers for ControllerResolver::getArguments() (iltar) - * feature #22782 [Debug][VarDumper] Remove the symfony_debug C extension (nicolas-grekas) - * feature #22771 [Workflow] Removed deprecated features (lyrixx) - * feature #22741 [Serializer] Remove deprecated DoctrineCache support (dunglas) - * feature #22733 Bump minimum version to PHP 7.1 for Symfony 4 (fabpot, dunglas, nicolas-grekas) - diff --git a/CHANGELOG-4.1.md b/CHANGELOG-4.1.md deleted file mode 100644 index 3a013e2504087..0000000000000 --- a/CHANGELOG-4.1.md +++ /dev/null @@ -1,586 +0,0 @@ -CHANGELOG for 4.1.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 4.1 minor versions. - -To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash -To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.1.0...v4.1.1 - -* 4.1.10 (2019-01-06) - - * bug #29494 [HttpFoundation] Fix request uri when it starts with double slashes (alquerci) - * bug #29697 [DI] Fixed wrong factory method in exception (Wojciech Gorczyca) - * bug #29679 [HttpKernel] Correctly Render Signed URIs Containing Fragments (zanbaldwin) - * bug #29754 Ensure final input of CommandTester works with default (Firehed) - * bug #29695 [Form] Do not ignore the choice groups for caching (vudaltsov) - * bug #29738 [Intl] handle null date and time types (xabbuh) - * bug #29708 [FrameworkBundle] access the container getting it from the kernel (xabbuh) - * bug #29704 [FrameworkBundle] improve errors in tests missing the BrowserKit component (xabbuh) - * bug #29617 [Console] Add specific replacement for help text in single command applications (codedmonkey) - * bug #29714 [Event Dispatcher] fixed 29703: TraceableEventDispatcher reset() callStack to null (mlievertz) - * bug #29597 [DI] fix reporting bindings on overriden services as unused (nicolas-grekas) - * bug #29639 [Yaml] detect circular references (xabbuh) - * bug #29626 [Routing] fix trailing slash redirections involving a trailing var (nicolas-grekas) - * bug #29411 [EventDispatcher] Revers event tracing order (ro0NL) - * bug #29533 Fixed public directory when configured in composer.json (alexander-schranz) - * bug #29619 [Console] OutputFormatter: move strtolower to createStyleFromString (ogizanagi) - * bug #29621 [Security] Prefer clone() over unserialize(serialize()) for user refreshment (chalasr) - * bug #29542 [Routing] fix dumping same-path routes with placeholders (nicolas-grekas) - * bug #29587 [Debug] ignore underscore vs backslash namespaces in DebugClassLoader (nicolas-grekas) - * bug #29584 [FrameworkBundle] fix describing routes with no controllers (nicolas-grekas) - * bug #29582 [DI] move RegisterServiceSubscribersPass before DecoratorServicePass (kbond) - * bug #29527 [TwigBridge][Form] Prevent multiple rendering of form collection prototypes (Shoplifter) - * bug #29571 [Yaml] ensures that the mb_internal_encoding is reset to its initial value (Jörn Lang) - * bug #29513 [Hackday][Serializer] Deserialization ignores argument type hint from phpdoc for array in constructor argument (karser) - * bug #29323 [Security] defer log message in guard authenticator (eschultz-magix) - * bug #29531 [Validator] Added IBAN format for Vatican City State (raulfraile) - * bug #29501 [Form] filter out invalid language values (xabbuh) - * bug #29307 [Form] Filter arrays out of scalar form types (nicolas-grekas) - * bug #29500 [Form] filter out invalid Intl values (xabbuh) - * bug #29499 [Validator] Fixed grouped composite constraints (HeahDude) - -* 4.1.9 (2018-12-06) - - * security #cve-2018-19790 [Security\Http] detect bad redirect targets using backslashes (xabbuh) - * security #cve-2018-19789 [Form] Filter file uploads out of regular form types (nicolas-grekas) - * bug #29436 [Cache] Fixed Memcached adapter doClear()to call flush() (raitocz) - * bug #29441 [Routing] ignore trailing slash for non-GET requests (nicolas-grekas) - * bug #29444 [Workflow] Fixed BC break for Workflow metadata (lyrixx) - * bug #29432 [DI] dont inline when lazy edges are found (nicolas-grekas) - * bug #29413 [Serializer] fixed DateTimeNormalizer to maintain microseconds when a different timezone required (rvitaliy) - * bug #29424 [Routing] fix taking verb into account when redirecting (nicolas-grekas) - * bug #29414 [DI] Fix dumping expressions accessing single-use private services (chalasr) - * bug #29375 [Validator] Allow `ConstraintViolation::__toString()` to expose codes that are not null or emtpy strings (phansys) - * bug #29376 [EventDispatcher] Fix eventListener wrapper loop in TraceableEventDispatcher (jderusse) - * bug #29386 undeprecate the single-colon notation for controllers (fbourigault) - * bug #29393 [DI] fix edge case in InlineServiceDefinitionsPass (nicolas-grekas) - * bug #29380 [Routing] fix greediness of trailing slash (nicolas-grekas) - * bug #29343 [Form] Handle all case variants of "nan" when parsing a number (mwhudson, xabbuh) - * bug #29373 [Routing] fix trailing slash redirection (nicolas-grekas) - * bug #29355 [PropertyAccess] calculate cache keys for property setters depending on the value (xabbuh) - * bug #29369 [DI] fix combinatorial explosion when analyzing the service graph (nicolas-grekas) - * bug #29349 [Debug] workaround opcache bug mutating "$this" !?! (nicolas-grekas) - -* 4.1.8 (2018-11-26) - - * bug #29318 [Console] Move back root exception to stack trace in verbose mode (chalasr) - * bug #29332 [PropertyAccess] make cache keys encoding bijective (nicolas-grekas) - * bug #29298 [Routing] fix trailing slash redirection when using RedirectableUrlMatcher (nicolas-grekas) - * bug #29297 [Routing] fix trailing slash redirection when using RedirectableUrlMatcher (nicolas-grekas) - * bug #29313 [PropertyAccessor] fix encoding of cache keys (nicolas-grekas) - * bug #28917 [DoctrineBridge] catch errors while converting to db values in data collector (alekitto) - * bug #29317 [WebProfiler] Detect non-file paths in file viewer (ro0NL) - * bug #29305 [EventDispatcher] Unwrap wrapped listeners internally (ro0NL) - * bug #27314 [DoctrineBridge] fix case sensitivity issue in RememberMe\DoctrineTokenProvider (PF4Public) - * bug #29310 [MonologBridge] Return empty list for unknown requests (ro0NL) - * bug #29308 [Translation] Use XLIFF source rather than resname when there's no target (thewilkybarkid) - * bug #26244 [BrowserKit] fixed BC Break for HTTP_HOST header (brizzz) - * bug #28147 [DomCrawler] exclude fields inside "template" tags (Gorjunov) - * bug #29222 [Dotenv] properly parse backslashes in unquoted env vars (xabbuh) - * bug #29256 [HttpFoundation] Fixed absolute Request URI with default port (thomasbisignani) - * bug #29274 [Routing] Remove duplicate schemes and methods for invokable controllers (claudusd) - * bug #29271 [HttpFoundation] Fix trailing space for mime-type with parameters (Sascha Dens) - * bug #29243 [Cache] fix optimizing Psr6Cache for AdapterInterface pools (nicolas-grekas) - * bug #29247 [DI] fix taking lazy services into account when dumping the container (nicolas-grekas) - * bug #29249 [Form] Fixed empty data for compound date interval (HeahDude) - * bug #29265 [Bridge/PhpUnit] Use composer to download phpunit (nicolas-grekas) - * bug #28769 [FrameworkBundle] deal with explicitly enabled workflow nodes (xabbuh) - * bug #29223 [Validator] Added the missing constraints instance checks (thomasbisignani) - * bug #28966 [PropertyAccessor] Fix unable to write to singular property using setter while plural adder/remover exist (karser) - * bug #29182 [Form] Fixed empty data for compound date types (HeahDude) - * bug #29191 [Routing] generate(null) should throw an exception (nicolas-grekas) - * bug #29185 [Form] Fixed keeping hash of equal \DateTimeInterface on submit (HeahDude) - * bug #29141 [Workflow] Fixed bug of buildTransitionBlockerList when many transition are enabled (Tetragramat, lyrixx) - * bug #29137 [Workflow][FrameworkBundle] fixed guard event names for transitions (destillat, lyrixx) - * bug #28731 [Form] invalidate forms on transformation failures (xabbuh) - * bug #29152 [Config] Unset key during normalization (ro0NL) - * bug #29165 [DI] align IniFileLoader to PHP bugfix #76965 (nicolas-grekas) - * bug #29115 Change button_widget class to btn-primary (neFAST) - * bug #29131 [Dotenv] dont use getenv() to read SYMFONY_DOTENV_VARS (nicolas-grekas) - * bug #29057 [HttpFoundation] replace any preexisting Content-Type headers (nicolas-grekas) - * bug #29076 [Serializer] Allow null values when denormalizing with constructor missing data (danut007ro) - * bug #29104 [DI] fix dumping inlined services (nicolas-grekas) - * bug #29054 [VarDumper] fix dump of closures created from callables (nicolas-grekas) - * bug #29102 [DI] fix GraphvizDumper ignoring inline definitions (nicolas-grekas) - * bug #29107 [DI] dont track classes/interfaces used to compute autowiring error messages (nicolas-grekas) - -* 4.1.7 (2018-11-03) - - * bug #28820 [DependencyInjection] Fix tags on multiple decorated service (Soner Sayakci) - * bug #29020 Fix ini_get() for boolean values (deguif) - * bug #28955 [Messenger] send using the routing_key for AMQP transport (nicolas-grekas) - * bug #28960 also clean away the NO_AUTO_CACHE_CONTROL_HEADER if we have no session (dbu) - * feature #28893 [TwigBundle] Fix usage of TwigBundle without FrameworkBundle (tgalopin) - * bug #28889 [Serializer] Reduce class discriminator overhead (fbourigault) - * bug #28861 [DependencyInjection] Skip empty proxy code (olvlvl) - * bug #28801 Convert InsufficientAuthenticationException to HttpException with 401 status code (vincentchalamon) - * bug #28840 add missing double-quotes to extra_fields output message (danielkay) - * bug #28838 [DI] Default undefined env to empty string during compile (ro0NL) - * bug #28863 [Process] Allow to pass non-string arguments to Process (vudaltsov) - * bug #28712 [Form] reverse transform RFC 3339 formatted dates (xabbuh) - * bug #28813 Fix for race condition in console output stream write (rudolfratusinski) - * bug #27772 [Console] Fixes multiselect choice question defaults in non-interactive mode (veewee) - * bug #28835 [FrameworkBundle] Setting missing default paths under BC layer (yceruto) - * bug #28760 [DI] fix dumping inline services again (nicolas-grekas) - * bug #28689 [Process] fix locking of pipe files on Windows (nicolas-grekas) - * bug #28704 [Form] fix multi-digit seconds fraction handling (xabbuh) - * bug #28793 [SecurityBundle] do not override custom access decision configs (xabbuh) - * bug #28783 [FrameworkBundle] add missing cache prefix seed attribute to XSD (xabbuh) - * bug #28072 [Security] Do not deauthenticate user when the first refreshed user has changed (gpekz) - * bug #28735 [FWBundle] Automatically enable PropertyInfo when using Flex (dunglas) - * bug #28751 [FrameworkBundle] Register messenger before the profiler (sroze) - -* 4.1.6 (2018-10-03) - - * bug #28604 [Finder] fixed root directory access for ftp/sftp wrapper (DerDu) - * bug #28688 [FWBundle] Throw if PropertyInfo is enabled, but the component isn't installed (dunglas) - * bug #28638 [Console] Fix clearing sections containing questions (chalasr) - * bug #28690 [FrameworkBundle] dont suggest hidden services in debug:container and debug:autow commands (nicolas-grekas) - * bug #28648 [PHPUnitBridge] Fix ClockMock microtime() format (acasademont) - * bug #28678 [DI] fix dumping setters before their inlined instances (nicolas-grekas) - * bug #28672 [DI] fix error in dumped container (nicolas-grekas) - * bug #28664 [Console] Don't return early as this bypasses the auto exit feature (duncan3dc) - -* 4.1.5 (2018-09-30) - - * bug #28636 [HttpFoundation] X-Accel-Mapping does not use HTTP key=value syntax (c960657) - * bug #28376 [TwigBundle] Fixed caching of templates in src/Resources//views on cache warmup (yceruto) - * bug #28565 [HttpFoundation][Security] forward locale and format to subrequests (nicolas-grekas) - * bug #28561 [Cache] prevent getting older entries when the version key is evicted (nicolas-grekas) - * bug #28562 [HttpFoundation] fix hidding warnings from session handlers (nicolas-grekas) - * bug #28545 [Console] Send the right exit code to console.terminate listeners (mpdude) - * bug #28553 [Debug] Fix false-positive "MicroKernelTrait::loadRoutes()" method is considered internal" (nicolas-grekas) - * bug #28466 [Form] fail reverse transforming invalid RFC 3339 dates (xabbuh) - * bug #28540 [Intl] parse numbers terminated with decimal separator (xabbuh) - * bug #28548 [Console] Fixed boxed table style with colspan (ro0NL) - * bug #28433 [HttpFoundation] Allow reuse of Session between requests if ID did not change (tgalopin) - * bug #28508 [Form] forward false label option to nested types (xabbuh) - * bug #28471 [MonologBridge] Re-add option option to ignore empty context and extra data (mpdude) - * bug #28464 [Form] forward the invalid_message option in date types (xabbuh) - * bug #28524 [PhpUnitBridge] fix disabling DeprecationErrorHandler using phpunit.xml file (soerenbernstein) - * bug #28512 [DI] fix infinite loop involving self-references in decorated services (nicolas-grekas) - * bug #28507 [DI] fix dumping lazy services (nicolas-grekas) - * bug #28469 [Form][TwigBridge] fix not displaying labels when value is false (xabbuh) - * bug #28495 [PhpUnitBridge] Implement startTest rather than startTestSuite (greg0ire) - * bug #28480 [DI] Detect circular references with ChildDefinition parent (Seb33300) - * bug #28497 [VarDumper] Fix global dump function return value for PHP7 (patrickcarlohickman) - * bug #28499 [Ldap] Use shut up operator on connection errors at ldap_start_tls (Andras Debreczeni) - * bug #28372 [Form] Fix DateTimeType html5 input format (franzwilding, mcfedr) - * bug #28396 [Intl] Blacklist Eurozone and United Nations in Region Data Generator (gregurco) - * bug #28418 [FrameworkBundle] Register the messenger data collector only when the profiler is enabled (pierredup) - * bug #28393 [Console] fixed corrupt error output for unknown multibyte short option (downace) - * bug #28411 [Debug] fix detecting overriden final/internal methods implemented using traits (nicolas-grekas) - * bug #28404 [Controller][ServiceValueResolver] Making method access case insensitive (nicoweb) - * bug #28401 [Console] Fix SymfonyQuestionHelper::askQuestion() with choice value as default (chalasr) - * bug #28388 [DI] configure inlined services before injecting them when dumping the container (nicolas-grekas) - * bug #28377 fix fopen flags (SpacePossum) - * bug #27764 [TwigBundle] Fixed caching of templates in default path on cache warmup (yceruto) - * bug #28366 [DI] Fix dumping some complex service graphs (nicolas-grekas) - * bug #27970 [FileValidator] Format file size in validation message according to binaryFormat option (jfredon) - * bug #28029 [TwigBundle] remove cache warmers when Twig cache is disabled (xabbuh) - * bug #28322 [Workflow] Make sure we do not run the next transition on an updated state (Nyholm) - * bug #28344 [HttpKernel][FrameworkBundle] Fix escaping of serialized payloads passed to test clients (nicolas-grekas) - * bug #28183 [WebProfilerBundle] fix wrong url when base path is the index (ismail1432) - * bug #28334 [FWB][Messenger] Revert "Move commands-specifics to a compiler pass in FWB" (sroze) - * bug #28328 [Messenger][FrameworkBundle] Move commands-specifics to a compiler pass in FWB (sroze) - -* 4.1.4 (2018-08-28) - - * bug #28278 [HttpFoundation] Fix unprepared BinaryFileResponse sends empty file (wackymole) - * bug #28284 [PhpUnitBridge] keep compat with composer 1.0 (nicolas-grekas) - * bug #28251 [HttpFoundation] Allow RedisCluster class for RedisSessionHandler (michaelperrin) - * bug #28241 [HttpKernel] fix forwarding trusted headers as server parameters (nicolas-grekas) - * bug #28220 [PropertyAccess] fix type error handling when writing values (xabbuh) - * bug #28249 [Cache] enable Memcached::OPT_TCP_NODELAY to fix perf of misses (nicolas-grekas) - * bug #28252 [DoctrineBridge] support __toString as documented for UniqueEntityValidator (dmaicher) - * bug #28216 [FrameworkBundle] `message_bus` alias public (sroze) - * bug #28113 [Form] Add help texts for checkboxes in horizontal bootstrap 4 forms (apfelbox) - * bug #28100 [Security] Call AccessListener after LogoutListener (chalasr) - * bug #28174 Remove the HTML5 validation from the profiler URL search form (Soullivaneuh) - * bug #28159 [DI] Fix autowire inner service (hason) - * bug #28060 [DI] Fix false-positive circular ref leading to wrong exceptions or infinite loops at runtime (nicolas-grekas) - * bug #28144 [HttpFoundation] fix false-positive ConflictingHeadersException (nicolas-grekas) - * bug #28152 [Translation] fix perf of lint:xliff command (nicolas-grekas) - * bug #28115 [Form] Remove extra .form-group wrapper around file widget in bootstrap 4 (MrMitch) - * bug #28120 [Routing] Fixed scheme redirecting for root path (twoleds) - * bug #28112 Fix CSS property typo (AhmedAbdulrahman) - * bug #28012 [PropertyInfo] Allow nested collections (jderusse) - * bug #28055 [PropertyInfo] Allow nested collections (jderusse) - * bug #28083 Remove the Expires header when calling Response::expire() (javiereguiluz) - -* 4.1.3 (2018-08-01) - - * security #cve-2018-14774 [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer (nicolas-grekas) - * security #cve-2018-14773 [HttpFoundation] Remove support for legacy and risky HTTP headers (nicolas-grekas) - * bug #28003 [HttpKernel] Fixes invalid REMOTE_ADDR in inline subrequest when configuring trusted proxy with subnet (netiul) - * bug #28007 [FrameworkBundle] fixed guard event names for transitions (destillat) - * bug #28045 [HttpFoundation] Fix Cookie::isCleared (ro0NL) - * bug #28080 [HttpFoundation] fixed using _method parameter with invalid type (Phobetor) - * bug #28059 [Messenger] Fix error message on undefined message class for non-subscriber handler (chalasr) - * bug #28052 [HttpKernel] Fix merging bindings for controllers' locators (nicolas-grekas) - * bug #28014 [Messenger] Fix chaining senders with their aliases (sroze) - -* 4.1.2 (2018-07-23) - - * bug #28005 [HttpKernel] Fixed templateExists on parse error of the template name (yceruto) - * bug #28013 [Messenger] Add missing typehint on chain sender (sroze) - * bug #27997 Serbo-Croatian has Serbian plural rule (kylekatarnls) - * bug #26193 Fix false-positive deprecation notices for TranslationLoader and WriteCheckSessionHandler (iquito) - * bug #27827 [Serializer] Supports nested abstract items (sroze) - * bug #27958 [Form] Remaining changes for bootstrap 4 file fields (apfelbox) - * bug #27919 [Form] Improve rendering of `file` field in bootstrap 4 (apfelbox) - * bug #27941 [WebProfilerBundle] Fixed icon alignment issue using Bootstrap 4.1.2 (jmsche) - * bug #27937 [HttpFoundation] reset callback on StreamedResponse when setNotModified() is called (rubencm) - * bug #27927 [HttpFoundation] Suppress side effects in 'get' and 'has' methods of NamespacedAttributeBag (webnet-fr) - * bug #27913 [EventDispatcher] Clear orphaned events on reset (acasademont) - * bug #27923 [Form/Profiler] Massively reducing memory footprint of form profiling pages... (VincentChalnot) - * bug #27918 [Console] correctly return parameter's default value on "--" (seschwar) - * bug #27826 [Serializer] Fix serialization of items with groups across entities and discrimination map (sroze) - * bug #27904 [Filesystem] fix lock file permissions (fritzmg) - * bug #27903 [Lock] fix lock file permissions (fritzmg) - * bug #27889 [Form] Replace .initialism with .text-uppercase. (vudaltsov) - * bug #27902 Fix the detection of the Process new argument (stof) - * bug #27885 [HttpFoundation] don't encode cookie name for BC (nicolas-grekas) - * bug #27782 [DI] Fix dumping ignore-on-uninitialized references to synthetic services (nicolas-grekas) - * bug #27435 [OptionResolver] resolve arrays (Doctrs) - * bug #27728 [TwigBridge] Fix missing path and separators in loader paths list on debug:twig output (yceruto) - * bug #27837 [PropertyInfo] Fix dock block lookup fallback loop (DerManoMann) - * bug #27848 [Workflow] Fixed BC break (lyrixx) - * bug #27758 [WebProfilerBundle] Prevent toolbar links color override by css (alcalyn) - * bug #27847 [Security] Fix accepting null as $uidKey in LdapUserProvider (louhde) - * bug #27820 [Messenger] Fix a bug when having more than one named handler per message subscriber (sroze) - * bug #27834 [DI] Don't show internal service id on binding errors (nicolas-grekas) - * bug #27831 Check for Hyper terminal on all operating systems. (azjezz) - * bug #27794 Add color support for Hyper terminal . (azjezz) - * bug #27809 [HttpFoundation] Fix tests: new message for status 425 (dunglas) - * bug #27618 [PropertyInfo] added handling of nullable types in PhpDoc (oxan) - * bug #27659 [HttpKernel] Make AbstractTestSessionListener compatible with CookieClearingLogoutHandler (thewilkybarkid) - * bug #27752 [Cache] provider does not respect option maxIdLength with versioning enabled (Constantine Shtompel) - * bug #27773 [Serializer] Class discriminator and serialization groups (sroze) - * bug #27710 [DependencyInjection] fix handling of empty DI extension configs (xabbuh) - * bug #27776 [ProxyManagerBridge] Fix support of private services (bis) (nicolas-grekas) - * bug #27714 [HttpFoundation] fix session tracking counter (nicolas-grekas, dmaicher) - * bug #27727 [Routing] Disallow object usage inside Route (paxal) - * bug #27736 [Routing] fix too much greediness in host-matching regex (nicolas-grekas) - * bug #27747 [HttpFoundation] fix registration of session proxies (nicolas-grekas) - * bug #27754 [HttpFoundation] missing namespace for RedisProxy (Bonfante) - * bug #27722 Redesign the Debug error page in prod (javiereguiluz) - * bug #27716 [DI] fix dumping deprecated service in yaml (nicolas-grekas) - -* 4.1.1 (2018-06-25) - - * bug #27626 [TwigBundle][DX] Only add the Twig WebLinkExtension if the WebLink component is enabled (thewilkybarkid) - * bug #27702 [TwigBundle] bump lowest deps to fix issue with "double-colon" controller service refs (nicolas-grekas) - * bug #27701 [SecurityBundle] Dont throw if "security.http_utils" is not found (nicolas-grekas) - * bug #27690 [DI] Resolve env placeholder in logs (ro0NL) - * bug #27687 [HttpKernel] fix argument's error messages in ServiceValueResolver (nicolas-grekas) - * bug #27614 [VarDumper] Fix dumping by splitting Server/Connection out of Dumper/ServerDumper (nicolas-grekas) - * bug #27681 [DI] Avoid leaking unused env placeholders (ro0NL) - * bug #26534 allow_extra_attributes does not throw an exception as documented (deviantintegral) - * bug #27664 [FrameworkBundle] Ignore keepQueryParams attribute when generating route redirect (vudaltsov) - * bug #27668 [Lock] use 'r+' for fopen (fixes issue on Solaris) (fritzmg) - * bug #27669 [Filesystem] fix file lock on SunOS (fritzmg) - * bug #27662 [HttpKernel] fix handling of nested Error instances (xabbuh) - * bug #27651 [Messenger] Fixed MessengerPass::guessHandledClasses return type (massimilianobraglia) - * bug #26845 [Config] Fixing GlobResource when inside phar archive (vworldat) - * bug #27382 [Form] Fix error when rendering a DateIntervalType form with exactly 0 weeks (krixon) - * bug #27309 Fix surrogate not using original request (Toflar) - * bug #27467 [HttpKernel] fix session tracking in surrogate master requests (nicolas-grekas) - * bug #27632 [HttpFoundation] Ensure RedisSessionHandler::updateTimestamp returns a boolean (MatTheCat) - * bug #27630 [Validator][Form] Remove BOM in some xlf files (gautierderuette) - * bug #27596 [Framework][Workflow] Added support for interfaces (vudaltsov) - * bug #27593 [ProxyManagerBridge] Fixed support of private services (nicolas-grekas) - * bug #27591 [VarDumper] Fix dumping ArrayObject and ArrayIterator instances (nicolas-grekas) - * bug #27528 [FrameworkBundle] give access to non-shared services when using test.service_container (nicolas-grekas) - * bug #27584 Avoid calling eval when there is no script embedded in the toolbar (stof) - * bug #27581 Fix bad method call with guard authentication + session migration (weaverryan) - * bug #27576 [Cache] Fix expiry comparisons in array-based pools (nicolas-grekas) - * bug #27566 [FrameworkBundle] fix for allowing single colon controller notation (dmaicher) - * bug #27556 Avoiding session migration for stateless firewall UsernamePasswordJsonAuthenticationListener (weaverryan) - * bug #27452 Avoid migration on stateless firewalls (weaverryan) - * bug #27568 [DI] Deduplicate generated proxy classes (nicolas-grekas) - * bug #27511 [Routing] fix matching host patterns, utf8 prefixes and non-capturing groups (nicolas-grekas) - * bug #27326 [Serializer] deserialize from xml: Fix a collection that contains the only one element (webnet-fr) - * bug #27562 [HttpKernel] Log/Collect exceptions at prio 0 (ro0NL) - * bug #27567 [PhpUnitBridge] Fix error on some Windows OS (Nsbx) - * bug #27357 [Lock] Remove released semaphore (jderusse) - * bug #27416 TagAwareAdapter over non-binary memcached connections corrupts memcache (Aleksey Prilipko) - * bug #27514 [Debug] Pass previous exception to FatalErrorException (pmontoya) - * bug #27516 Revert "bug #26138 [HttpKernel] Catch HttpExceptions when templating is not installed (cilefen)" (nicolas-grekas) - * bug #27501 [FrameworkBundle] Fix test-container on kernel reboot, revert to returning the real container from Client::getContainer() (nicolas-grekas) - * bug #27472 [DI] Ignore missing tree root nodes on validate (ro0NL) - * bug #27458 [WebProfilerBundle] fixed getSession when no session has been set deprecation warnings (GregOriol) - * bug #27318 [Cache] memcache connect should not add duplicate entries on sequential calls (Aleksey Prilipko) - * bug #27498 [Routing] Don't reorder past variable-length placeholders (nanocom, nicolas-grekas) - * bug #27496 [DebugBundle] DebugBundle::registerCommands should be noop (ogizanagi) - * bug #27485 [BrowserKit] Fix a BC break in Client affecting Panthère (dunglas) - * bug #27470 [DI] Remove default env type check on validate (ro0NL) - * bug #27454 [FrameworkBundle][TwigBridge] Fix BC break from strong dependency on CSRF token storage (tgalopin) - * bug #27389 [Serializer] Fix serializer tries to denormalize null values on nullable properties (ogizanagi) - * bug #27272 [FrameworkBundle] Change priority of AddConsoleCommandPass to TYPE_BEFORE_REMOVING (upyx) - * bug #27396 [HttpKernel] fix registering IDE links (nicolas-grekas) - * bug #26973 [HttpKernel] Set first trusted proxy as REMOTE_ADDR in InlineFragmentRenderer. (kmadejski) - * bug #27303 [Process] Consider "executable" suffixes first on Windows (sanmai) - * bug #27297 Triggering RememberMe's loginFail() when token cannot be created (weaverryan) - -* 4.1.0 (2018-05-30) - - * bug #27420 Revert "feature #26702 Mark ExceptionInterfaces throwable (ostrolucky)" (nicolas-grekas) - * bug #27415 Insert correct parameter_bag service in AbstractController (curry684) - -* 4.1.0-BETA3 (2018-05-26) - - * bug #27388 [Routing] Account for greediness when merging route patterns (nicolas-grekas) - * bug #27344 [HttpKernel] reset kernel start time on reboot (kiler129) - * bug #27365 [Serializer] Check the value of enable_max_depth if defined (dunglas) - * bug #27358 [PhpUnitBridge] silence some stderr outputs (ostrolucky) - * bug #27366 [DI] never inline lazy services (nicolas-grekas) - * bug #27352 Remove reference to the test container after kernel shutdown (stof) - * bug #27350 [HttpKernel] fix deprecation in AbstractTestSessionListener (alekitto) - * bug #27367 [FrameworkBundle] cleanup generated test container (nicolas-grekas) - * bug #27379 [FrameworkBundle] Fix using test.service_container when Client is rebooted (nicolas-grekas) - * bug #27364 [DI] Fix bad exception on uninitialized references to non-shared services (nicolas-grekas) - * bug #27359 [HttpFoundation] Fix perf issue during MimeTypeGuesser intialization (nicolas-grekas) - * security #cve-2018-11408 [SecurityBundle] Fail if security.http_utils cannot be configured - * security #cve-2018-11406 clear CSRF tokens when the user is logged out - * security #cve-2018-11385 migrating session for UsernamePasswordJsonAuthenticationListener - * security #cve-2018-11385 migrating session for UsernamePasswordJsonAuthenticationListener - * security #cve-2018-11385 Adding session authentication strategy to Guard to avoid session fixation - * security #cve-2018-11385 Adding session strategy to ALL listeners to avoid *any* possible fixation - * security #cve-2018-11386 [HttpFoundation] Break infinite loop in PdoSessionHandler when MySQL is in loose mode - * bug #27341 [WebProfilerBundle] Fixed validator/dump trace CSS (yceruto) - * bug #27337 [FrameworkBundle] fix typo in CacheClearCommand (emilielorenzo) - * bug #27292 [Serializer] Fix and improve constraintViolationListNormalizer's RFC7807 compliance (dunglas) - -* 4.1.0-BETA2 (2018-05-21) - - * bug #27312 Supress deprecation notices thrown when getting private servies from container in tests (arderyp) - * feature #27275 [Messenger] Allow to scope handlers per bus (ogizanagi, sroze) - * bug #27264 [Validator] Use strict type in URL validator (mimol91) - * bug #27267 [DependencyInjection] resolve array env vars (jamesthomasonjr) - * bug #26781 [Form] Fix precision of MoneyToLocalizedStringTransformer's divisions on transform() (syastrebov) - * bug #27270 [Routing] Fix adding name prefix to canonical route names (ismail1432) - * bug #27286 [Translation] Add Occitan plural rule (kylekatarnls) - * bug #27271 [DI] Allow defining bindings on ChildDefinition (nicolas-grekas) - * bug #27246 Disallow invalid characters in session.name (ostrolucky) - * feature #27230 [Messenger] Select alternatives on missing receiver arg or typo (yceruto) - * bug #27287 [PropertyInfo] fix resolving parent|self type hints (nicolas-grekas) - * bug #27281 [HttpKernel] Fix dealing with self/parent in ArgumentMetadataFactory (fabpot) - * bug #24805 [Security] Fix logout (MatTheCat) - * bug #27265 [DI] Shared services should not be inlined in non-shared ones (nicolas-grekas) - * bug #27141 [Process] Suppress warnings when open_basedir is non-empty (cbj4074) - * bug #27250 [Session] limiting :key for GET_LOCK to 64 chars (oleg-andreyev) - * feature #27128 [Messenger] Middleware factories support in config (ogizanagi) - * bug #27214 [HttpKernel] Fix services are no longer injected into __invoke controllers method (ogizanagi) - * bug #27237 [Debug] Fix populating error_get_last() for handled silent errors (nicolas-grekas) - * bug #27232 [Cache][Lock] Fix usages of error_get_last() (nicolas-grekas) - * bug #27236 [Filesystem] Fix usages of error_get_last() (nicolas-grekas) - * feature #27202 [Messenger] Improve the profiler panel (ogizanagi) - * bug #27191 [DI] Display previous error messages when throwing unused bindings (nicolas-grekas) - * bug #27231 [FrameworkBundle] Fix cache:clear on vagrant (nicolas-grekas) - * bug #27222 [WebProfilerBundle][Cache] Fix misses calculation when calling getItems (fsevestre) - * bug #27227 [HttpKernel] Handle NoConfigurationException "onKernelException()" (nicolas-grekas) - * feature #27034 [Messenger][DX] Uses custom method names for handlers (sroze) - * bug #27228 [Messenger] Remove autoconfiguration for Sender/ReceiverInterface (kbond) - * bug #27229 [Messenger] Rename tag attribute "name" by "alias" (yceruto) - * bug #27224 [Messenger] Make sure default receiver name is set before command configuration (yceruto) - * feature #27225 [Messenger] Autoconfiguring TransportFactoryInterface classes (yceruto) - * bug #27220 [Messenger] Fix new AMQP Transport test with Envelope & fix contract (ogizanagi) - * bug #27184 [Messenger] Fix return senders based on the message parents/interfaces (yceruto) - * feature #27182 [Messenger] Re-introduce wrapped message configuration (with fix) (sroze, ogizanagi) - * bug #27209 [Workflow] add is deprecated since Symfony 4.1. Use addWorkflow() instead (xkobal) - * feature #26803 [Messenger] Add debug:messenger CLI command (ro0NL, sroze) - * bug #27189 [Profiler] Fix dump makes toolbar disappear (ogizanagi) - * bug #27199 [Messenger] Fix default bus name (ogizanagi) - * bug #27198 [Messenger] Fix the transport factory after moving it (sroze) - * bug #27197 [Messenger] Fix AMQP Transport factory & TransportFactoryInterface (ogizanagi) - * bug #27196 [Messenger] Fix AMQP Transport (yceruto) - -* 4.1.0-BETA1 (2018-05-07) - - * feature #26945 [Messenger] Support configuring messages when dispatching (ogizanagi) - * feature #27168 [HttpKernel] Add Kernel::getAnnotatedClassesToCompile() (nicolas-grekas) - * feature #27170 Show the deprecations tab by default in the logger panel (javiereguiluz) - * feature #27130 [Messenger] Add a new time limit receiver (sdelicata) - * feature #27104 [DX] Redirect to proper Symfony version documentation (noniagriconomie) - * feature #27105 [Serializer] Add ->hasCacheableSupportsMethod() to CacheableSupportsMethodInterface (nicolas-grekas) - * feature #24896 Add CODE_OF_CONDUCT.md (egircys) - * feature #27092 [Workflow] "clear()" instead of "reset()" (nicolas-grekas) - * feature #26655 [WebProfilerBundle] Make WDT follow ajax requests if header set (jeffreymb) - * feature #27049 [Serializer] Cache the normalizer to use when possible (dunglas, nicolas-grekas) - * feature #27062 [SecurityBundle] Register a `UserProviderInterface` alias if one provider only (sroze) - * feature #27065 [DI][Routing] Allow invokable objects to be used as PHP-DSL loaders (aurimasniekis) - * feature #26975 [Messenger] Add a memory limit option for `ConsumeMessagesCommand` (sdelicata) - * feature #26864 [Messenger] Define multiple buses from the `framework.messenger.buses` configuration (sroze) - * feature #27017 [Serializer] Allow to access to the context and various other infos in callbacks and max depth handler (dunglas) - * feature #26832 [MonologBridge] Added WebSubscriberProcessor to ease processor configuration (lyrixx) - * feature #24699 [HttpFoundation] Add HeaderUtils class (c960657) - * feature #26791 [BrowserKit] Bypass Header Informations (cfjulien) - * feature #26825 [Form] Add choice_translation_locale option for Intl choice types (yceruto, fabpot) - * feature #26921 [DI][FrameworkBundle] Hide service ids that start with a dot (nicolas-grekas) - * feature #23659 [HttpKernel] LoggerDataCollector: splitting logs on different sub-requests (vtsykun) - * feature #26768 [DI] Allow autoconfigured calls in PHP (Gary PEGEOT, GaryPEGEOT) - * feature #26833 [HttpKernel] Added support for timings in ArgumentValueResolvers (iltar) - * feature #26770 Do not normalize array keys in twig globals (lstrojny) - * feature #26787 [Security] Make security.providers optional (MatTheCat) - * feature #26970 [VarDumper] Add dd() helper == dump() + exit() (nicolas-grekas) - * feature #26941 [Messenger] Allow to configure the transport (sroze) - * feature #26632 [Messenger] Add AMQP adapter (sroze) - * feature #26863 [Console] Support iterable in SymfonyStyle::write/writeln (ogizanagi) - * feature #26847 [Console] add support for iterable in output (Tobion) - * feature #26660 [SecurityBundle] allow using custom function inside allow_if expressions (dmaicher) - * feature #26096 [HttpFoundation] Added a migrating session handler (rossmotley) - * feature #26528 [Debug] Support any Throwable object in FlattenException (derrabus) - * feature #26811 [PhpUnitBridge] Search for other SYMFONY_* env vars in phpunit.xml then phpunit.xml.dist (lyrixx) - * feature #26800 [PhpUnitBridge] Search for SYMFONY_PHPUNIT_REMOVE env var in phpunit.xml then phpunit.xml.dist (lyrixx) - * feature #26684 [Messenger] Remove the Doctrine middleware configuration from the FrameworkBundle (sroze) - * feature #21856 [LDAP] Allow adding and removing values to/from multi-valued attributes (jean-gui) - * feature #26767 [Form] ability to set rounding strategy for MoneyType (syastrebov) - * feature #23707 [Monolog Bridge][DX] Add a Monolog activation strategy for ignoring specific HTTP codes (simshaun, fabpot) - * feature #26685 [Messenger] Add a `MessageHandlerInterface` (multiple messages + auto-configuration) (sroze) - * feature #26648 [Messenger] Added a middleware that validates messages (Nyholm) - * feature #26475 [HttpFoundation] split FileException into specialized ones about upload handling (fmata) - * feature #26702 Mark ExceptionInterfaces throwable (ostrolucky) - * feature #26656 [Workflow][Registry] Added a new 'all' method (alexpozzi, lyrixx) - * feature #26693 [Console] Add box-double table style (maidmaid) - * feature #26698 [Console] Use UTF-8 bullet for listing (ro0NL) - * feature #26682 Improved the lint:xliff command (javiereguiluz) - * feature #26681 Allow to easily ask Symfony not to set a response to private automatically (Toflar) - * feature #26627 [DI] Add runtime service exceptions to improve the error message when controller arguments cannot be injected (nicolas-grekas) - * feature #26504 [FrameworkBundle] framework.php_errors.log now accept a log level (Simperfit) - * feature #26498 Allow "json:" env var processor to accept null value (mcfedr) - * feature #25928 [DI] Allow binary values in parameters. (bburnichon) - * feature #26647 [Messenger] Add a middleware that wraps all handlers in one Doctrine transaction. (Nyholm) - * feature #26668 [WebProfilerBundle] Live duration of AJAX request (ostrolucky) - * feature #26650 [Messenger] Clone messages to show in profiler (Nyholm) - * feature #26281 [FrameworkBundle] keep query in redirect (Simperfit) - * feature #26665 Improved the Ajax profiler panel when there are exceptions (javiereguiluz) - * feature #26654 [VarDumper] Provide binary, allowing to start a server at any time (ogizanagi) - * feature #26332 Add a data_help method in Form (mpiot, Nyholm) - * feature #26671 More compact display of vendor code in exception pages (javiereguiluz) - * feature #26502 [Form] Add Bootstrap 4 style for field FileType (zenmate) - * feature #23888 [DI] Validate env vars in config (ro0NL) - * feature #26658 Adding support to bind scalar values to controller arguments (weaverryan) - * feature #26651 [Workflow] Added a TransitionException (andrewtch, lyrixx) - * feature #23831 [VarDumper] Introduce a new way to collect dumps through a server dumper (ogizanagi, nicolas-grekas) - * feature #26220 [HttpFoundation] Use parse_str() for query strings normalization (nicolas-grekas) - * feature #24411 [Messenger] Add a new Messenger component (sroze) - * feature #22150 [Serializer] Added a ConstraintViolationListNormalizer (lyrixx) - * feature #26639 [SecurityBundle] Added an alias from RoleHierarchyInterface to security.role_hierarchy (lyrixx) - * feature #26636 [DI] deprecate TypedReference::canBeAutoregistered() and getRequiringClass() (nicolas-grekas) - * feature #26445 [Serializer] Ignore comments when decoding XML (q0rban) - * feature #26284 [Routing] allow no-slash root on imported routes (nicolas-grekas) - * feature #26092 [Workflow] Add a MetadataStore to fetch some metadata (lyrixx) - * feature #26121 [FrameworkBundle] feature: add the ability to search a route (Simperfit) - * feature #25197 [FrameworkBundle][TwigBridge] make csrf_token() usable without forms (xabbuh) - * feature #25631 [DI] Service decoration: autowire the inner service (dunglas) - * feature #26076 [Workflow] Add transition blockers (d-ph, lyrixx) - * feature #24363 [Console] Modify console output and print multiple modifyable sections (pierredup) - * feature #26381 Transform both switchToXHR() and removeXhr() to xmlHttpRequest() (Simperfit) - * feature #26449 Make ProgressBar::setMaxSteps public (ostrolucky) - * feature #26308 [Config] Introduce BuilderAwareInterface (ro0NL) - * feature #26518 [Routing] Allow inline definition of requirements and defaults (nicolas-grekas) - * feature #26143 [Routing] Implement i18n routing (frankdejonge, nicolas-grekas) - * feature #26564 [HttpFoundation] deprecate call to Request::getSession() when Request::hasSession() returns false (fmata) - * feature #26408 Readd 'form_label_errors' block to disable errors on form labels (birkof) - * feature #25456 [Console] Make pretty the `box` style table (maidmaid) - * feature #26499 [FrameworkBundle] Allow fetching private services from test clients (nicolas-grekas) - * feature #26509 [BrowserKit] Avoid nullable values in some Client's methods (ossinkine) - * feature #26288 [FrameworkBundle] show the unregistered command warning at the end of the list command (Simperfit) - * feature #26520 Added some HTML5 features to the Symfony Profiler (javiereguiluz) - * feature #26398 [WebProfilerBundle] Display the missing translation panel by default (javiereguiluz) - * feature #23409 [Security] AuthenticationUtils::getLastUsername() return type inconsistency (vudaltsov) - * feature #26439 [Console] [DX] Fix command description/help display (noniagriconomie) - * feature #26372 Revert "feature #24763 [Process] Allow writing portable "prepared" command lines (Simperfit)" (nicolas-grekas) - * feature #26223 [FrameworkBundle] Add command to delete an item from a cache pool (pierredup) - * feature #26341 Autoconfigure service locator tag (apfelbox) - * feature #26330 [FORM] Fix HTML errors. (Nyholm) - * feature #26334 [SecurityBundle] Deprecate switch_user.stateless config node (chalasr) - * feature #26304 [Routing] support scheme requirement without redirectable dumped matcher (Tobion) - * feature #26283 [Routing] Redirect from trailing slash to no-slash when possible (nicolas-grekas) - * feature #25732 [Console] Add option to automatically run suggested command if there is only 1 alternative (pierredup) - * feature #26085 Deprecate bundle:controller:action and service:method notation (Tobion) - * feature #26175 [Security] Add configuration for Argon2i encryption (CoalaJoe) - * feature #26075 [Validator] Deprecate use of `Locale` validation constraint without setting "canonicalize" option to `true` (phansys) - * feature #26218 [MonologBridge] Allow to change level format in ConsoleFormatter (ostrolucky) - * feature #26232 [Lock] Add a TTL to refresh lock (jderusse) - * feature #26108 [Serializer] Add a MaxDepth handler (dunglas) - * feature #24778 [BrowserKit] add a way to switch to ajax for one request (Simperfit) - * feature #25605 [PropertyInfo] Added support for extracting type from constructor (lyrixx) - * feature #24763 [Process] Allow writing portable "prepared" command lines (Simperfit) - * feature #25218 [Serializer] add a constructor arguement to return csv always as collection (Simperfit) - * feature #25369 [Serializer] add a context key to return always as collection for XmlEncoder (Simperfit) - * feature #26213 [FrameworkBundle] Add support to 307/308 HTTP status codes in RedirectController (ZipoKing) - * feature #26149 Added support for name on the unit node (Nyholm) - * feature #24308 [Validator] support protocolless urls validation (MyDigitalLife) - * feature #26059 [Routing] Match 77.7x faster by compiling routes in one regexp (nicolas-grekas) - * feature #22447 [WebProfilerBundle] Imply forward request by a new X-Previous-Debug-Token header (ro0NL) - * feature #26152 [Intl] Add polyfill for Locale::canonicalize() (nicolas-grekas) - * feature #26073 [DoctrineBridge] Add support for datetime immutable types in doctrine type guesser (jvasseur) - * feature #26079 [Workflow] Remove constraints on transition/place name + Updated Dumper (lyrixx) - * feature #23617 [PropertyInfo] Add hassers for accessors prefixes (sebdec) - * feature #25997 Always show all deprecations except legacy ones when not weak (greg0ire) - * feature #25582 [Form] Support \DateTimeImmutable (vudaltsov) - * feature #24705 [Workflow] Add PlantUML dumper to workflow:dump command (Plopix) - * feature #24508 [Serializer] Fix security issue on CsvEncoder about CSV injection (welcoMattic) - * feature #25772 [Security] The AuthenticationException should implements Security's ExceptionInterface (sroze) - * feature #25164 [WebProfilerBundle] Improve controller linking (ro0NL) - * feature #22353 [Validator] Add `canonicalize` option for `Locale` validator (phansys) - * feature #26036 Added support for getting default values in Accept headers (javiereguiluz) - * feature #25780 [TwigBundle] Deprecating "false" in favor of "kernel.debug" as default value of "strict_variable" (yceruto) - * feature #23508 Deprecated the AdvancedUserInterface (iltar) - * feature #24781 [HttpFoundation] RedisSessionHandler (dkarlovi) - * feature #26028 Unwrap errors in FlattenException (derrabus) - * feature #25892 Adding an array adapter (weaverryan) - * feature #24894 [FrameworkBundle] add a notice when passing a routerInterface without warmupInterface in RouterCacheWarmer (Simperfit) - * feature #24632 [DependencyInjection] Anonymous services in PHP DSL (unkind) - * feature #25836 [HttpKernel] Make session-related services extra-lazy (nicolas-grekas) - * feature #25775 Introduce signaled process specific exception class (Soullivaneuh) - * feature #22253 [Config] allow changing the path separator (bburnichon) - * feature #25493 [Serializer] `default_constructor_arguments` context option for denormalization (Nek-) - * feature #25839 [SecurityBundle] Deprecate in_memory.user abstract service (chalasr) - * feature #24392 Display orphaned events in profiler (kejwmen) - * feature #25275 [DI] Allow for invokable event listeners (ro0NL) - * feature #25627 [DI] Add a simple CSV env var processor (dunglas) - * feature #25092 [Security] #25091 add target user to SwitchUserListener (jwmickey) - * feature #24777 [TwigBundle] Added priority to twig extensions (Brunty) - * feature #25710 [FrameworkBundle] add cache.app.simple psr simple cache (dmaicher) - * feature #25669 [Security] Fail gracefully if the security token cannot be unserialized from the session (thewilkybarkid) - * feature #25504 [Validator] Add option to pass custom values to Expression validator (ostrolucky) - * feature #25701 [FrameworkBundle] add autowiring aliases for TranslationReaderInterface, ExtractorInterface & TranslationWriterInterface (Dennis Langen) - * feature #25516 [Validator] Deprecated "checkDNS" option in Url constraint (ro0NL) - * feature #25588 Move SecurityUserValueResolver to security-http (chalasr) - * feature #25629 [Process] Make `PhpExecutableFinder` look for the `PHP_BINARY` env var (nicolas-grekas) - * feature #25562 allow autowire for http_utils class (do-see) - * feature #25478 [FrameworkBundle] add email_validation_mode option (xabbuh) - * feature #25366 [HttpKernel] Decouple exception logging from rendering (ro0NL) - * feature #25450 [PropertyAccess] add more information to NoSuchPropertyException Message (Simperfit) - * feature #25148 Pr/workflow name as graph label (shdev) - * feature #25324 [HttpFoundation] Incorrect documentation and method name for UploadedFile::getClientSize() (Simperfit) - * feature #24738 [FrameworkBundle][Routing] Use a PSR-11 container in FrameworkBundle Router (ogizanagi) - * feature #25439 Add ControllerTrait::getParameter() (chalasr) - * feature #25332 [VarDumper] Allow VarDumperTestTrait expectation to be non-scalar (romainneutron) - * feature #25301 [Console] Add box style table (maidmaid) - * feature #25415 [FrameworkBundle] Add atom editor to ide config (lexcast) - * feature #24442 [Validator] Html5 Email Validation (PurpleBooth) - * feature #25288 [DI][FrameworkBundle] Add PSR-11 "ContainerBag" to access parameters as-a-service (nicolas-grekas, sroze) - * feature #25290 [FrameworkBundle] debug:autowiring: don't list FQCN when they are aliased (nicolas-grekas) - * feature #24375 [Serializer] Serialize and deserialize from abstract classes (sroze) - * feature #25346 [DoctrineBridge] DoctrineDataCollector comments the non runnable part of the query (Simperfit) - * feature #24216 added clean option to assets install command (robinlehrmann) - * feature #25142 [Process] Create a "isTtySupported" static method (nesk) - * feature #24751 [Workflow] Introduce a Workflow interface (Simperfit) - * feature #25293 [Routing] Parse PHP constants in YAML routing files (ostrolucky) - * feature #25295 [Translation] Parse PHP constants in YAML translation files (ostrolucky) - * feature #25294 [Serializer] Parse PHP constants in YAML mappings (ostrolucky) - * feature #24637 [FrameworkBundle] Improve the DX of TemplateController when using SF 4 (dunglas) - * feature #25178 [Routing] Allow to set name prefixes from the configuration (sroze) - * feature #25237 [VarDumper] add a GMP caster in order to cast GMP resources into string or integer (Simperfit) - * feature #25166 [WebProfilerBundle] Expose dotenv variables (ro0NL) - * feature #24785 [Profiler][Translation] Logging false by default and desactivated when using the profiler (Simperfit) - * feature #24826 [FrameworkBundle] Allow to pass a logger instance to the Router (ogizanagi) - * feature #24937 [DependencyInjection] Added support for variadics in named arguments (PabloKowalczyk) - * feature #24819 [Console] add setInputs to ApplicationTester and share some code (Simperfit) - * feature #25131 [SecurityBundle][Security][Translation] trigger some deprecations for legacy methods (xabbuh) - diff --git a/CHANGELOG-4.2.md b/CHANGELOG-4.2.md deleted file mode 100644 index 40e3209d1d568..0000000000000 --- a/CHANGELOG-4.2.md +++ /dev/null @@ -1,668 +0,0 @@ -CHANGELOG for 4.2.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 4.2 minor versions. - -To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash -To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.2.0...v4.2.1 - -* 4.2.10 (2019-06-26) - - * bug #31972 Add missing rendering of form help block. (alexsegura) - * bug #32137 [HttpFoundation] fix accessing session bags (xabbuh) - * bug #32164 [EventDispatcher] collect called listeners information only once (xabbuh) - * bug #32173 [FrameworkBundle] Fix calling Client::getProfile() before sending a request (dunglas) - * bug #32163 [DoctrineBridge] Fix type error (norkunas) - * bug #32170 [Security/Core] Don't use ParagonIE_Sodium_Compat (nicolas-grekas) - * bug #32094 [Validator] Use LogicException for missing Property Access Component in comparison constraints (Lctrs) - * bug #32123 [Form] fix translation domain (xabbuh) - * bug #32116 [FrameworkBundle] tag the FileType service as a form type (xabbuh) - * bug #32090 [Debug] workaround BC break in PHP 7.3 (nicolas-grekas) - * bug #32076 [Lock] Fix PDO prune not called (jderusse) - * bug #32071 Fix expired lock not cleaned (jderusse) - * bug #32057 [HttpFoundation] Fix SA/phpdoc JsonResponse (ro0NL) - * bug #32025 SimpleCacheAdapter fails to cache any item if a namespace is used (moufmouf) - * bug #32037 [Form] validate composite constraints in all groups (xabbuh) - * bug #32007 [Serializer] Handle true and false appropriately in CSV encoder (battye) - * bug #32000 [Routing] fix absolute url generation when scheme is not known (Tobion) - * bug #32024 [VarDumper] fix dumping objects that implement __debugInfo() (nicolas-grekas) - * bug #31962 Fix reporting unsilenced deprecations from insulated tests (nicolas-grekas) - * bug #31925 [Form] fix usage of legacy TranslatorInterface (nicolas-grekas) - * bug #31908 [Validator] fix deprecation layer of ValidatorBuilder (nicolas-grekas) - * bug #31894 Fix wrong requirements for ocramius/proxy-manager in root composer.json (henrikvolmer) - * bug #31865 [Form] Fix wrong DateTime on outdated ICU library (aweelex) - * bug #31879 [Cache] Pass arg to get callback everywhere (fancyweb) - * bug #31863 [HttpFoundation] Fixed case-sensitive handling of cache-control header in RedirectResponse constructor (Ivo) - * bug #31869 Fix json-encoding when JSON_THROW_ON_ERROR is used (nicolas-grekas) - * bug #31868 [HttpKernel] Fix handling non-catchable fatal errors (nicolas-grekas) - * bug #31860 [HttpFoundation] work around PHP 7.3 bug related to json_encode() (nicolas-grekas) - * bug #31407 [Security] added support for updated "distinguished name" format in x509 authentication (Robert Kopera) - * bug #31786 [Translation] Fixed case sensitivity of lint:xliff command (javiereguiluz) - * bug #31757 [DomCrawler] Fix type error with null Form::$currentUri (chalasr) - * bug #31654 [HttpFoundation] Do not set X-Accel-Redirect for paths outside of X-Accel-Mapping (vilius-g) - -* 4.2.9 (2019-05-28) - - * bug #31584 [Workflow] Do not trigger extra guards (lyrixx) - * bug #31632 [Messenger] Use "real" memory usage to honor --memory-limit (chalasr) - * bug #31599 [Translation] Fixed issue with new vs old TranslatorInterface in TranslationDataCollector (althaus) - * bug #31349 [WebProfilerBundle] Use absolute URL for profiler links (Alumbrados) - * bug #31541 [DI] fix using bindings with locators of service subscribers (nicolas-grekas) - * bug #31568 [Process] Fix infinite waiting for stopped process (mshavliuk) - * bug #31551 [ProxyManager] isProxyCandidate() does not take into account interfaces (andrerom) - * bug #31335 [Doctrine] Respect parent class contract in ContainerAwareEventManager (Koc) - * bug #31421 [Routing][AnnotationClassLoader] fix utf-8 encoding in default route name (przemyslaw-bogusz) - * bug #31510 Use the current working dir as default first arg in 'link' binary (lyrixx) - * bug #31524 [HttpFoundation] prevent deprecation when filesize matches error code (xabbuh) - * bug #31535 [Debug] Wrap call to require_once in a try/catch (lyrixx) - * bug #31477 [PropertyAccess] Add missing property to PropertyAccessor (vudaltsov) - * bug #31479 [Cache] fix saving unrelated keys in recursive callback calls (nicolas-grekas) - * bug #31438 [Serializer] Fix denormalization of object with variadic constructor typed argument (ajgarlag) - * bug #31445 [Messenger] Making cache rebuild correctly when message subscribers change (weaverryan) - * bug #31442 [Validator] Fix finding translator parent definition in compiler pass (deguif) - * bug #31475 [HttpFoundation] Allow set 'None' on samesite cookie flag (markitosgv) - * bug #31456 Remove deprecated usage of some Twig features (fabpot) - * bug #31207 [Routing] Fixed unexpected 404 NoConfigurationException (yceruto) - * bug #31261 [Console] Commands with an alias should not be recognized as ambiguous when using register (Simperfit) - * bug #31371 [DI] Removes number of elements information in debug mode (jschaedl) - * bug #31418 [FrameworkBundle] clarify the possible class/interface of the cache (xabbuh) - * bug #31411 [Intl] Fix root fallback locale (ro0NL) - * bug #31377 [Console] Fix auto-complete for ChoiceQuestion (multi-select answers) (battye) - * bug #31380 [WebProfilerBundle] Don't filter submitted IP values (javiereguiluz) - -* 4.2.8 (2019-05-01) - - * bug #31338 Revert "bug #30620 [FrameworkBundle][HttpFoundation] make session service resettable (dmaicher)" (nicolas-grekas) - * bug #31326 fix ConsoleFormatter - call to a member function format() on string (keksa) - * bug #31331 [Workflow] Fixed dumping when many transition with same name exist (lyrixx) - * bug #31302 [FramworkBundle] mark any env vars found in the ide setting as used (nicolas-grekas) - * bug #31290 [TwigBundle] Use the apply tag instead of the filter tag (greg0ire) - * bug #31275 [Translator] Preserve default domain when extracting strings from php files (Stadly) - * bug #31240 Fix url matcher edge cases with trailing slash (arjenm) - * bug #31201 [Form] resolve class name parameters (xabbuh) - * bug #31213 [WebProfilerBundle] Intercept redirections only for HTML format (javiereguiluz) - * bug #31210 [PhpUnitBridge] fix reading phpunit.xml on bootstrap (nicolas-grekas) - * bug #31023 [Routing] Fix route URL generation in CLI context (X-Coder264) - * bug #31117 [FrameworkBundle] fix math depth handler configuration (Raulnet) - * bug #31182 [Routing] fix trailing slash matching with empty-matching trailing vars (nicolas-grekas) - * bug #31167 [Routing] fix matching trailing vars with defaults (nicolas-grekas) - * bug #31164 [Validator] fix LegacyTranslatorProxy (nicolas-grekas) - * bug #31156 [FrameworkBundle] call method with Translator component only (xabbuh) - -* 4.2.7 (2019-04-17) - - * bug #31107 [Routing] fix trailing slash redirection with non-greedy trailing vars (nicolas-grekas) - * bug #31108 [FrameworkBundle] decorate the ValidatorBuilder's translator with LegacyTranslatorProxy (nicolas-grekas) - * bug #31121 [HttpKernel] Fix get session when the request stack is empty (yceruto) - * bug #31084 [HttpFoundation] Make MimeTypeExtensionGuesser case insensitive (vermeirentony) - * bug #31142 Revert "bug #30423 [Security] Rework firewall's access denied rule (dimabory)" (chalasr) - * security #cve-2019-10910 [DI] Check service IDs are valid (nicolas-grekas) - * security #cve-2019-10909 [FrameworkBundle][Form] Fix XSS issues in the form theme of the PHP templating engine (stof) - * security #cve-2019-10912 [Cache][PHPUnit Bridge] Prevent destructors with side-effects from being unserialized (nicolas-grekas) - * security #cve-2019-10911 [Security] Add a separator in the remember me cookie hash (pborreli) - * security #cve-2019-10913 [HttpFoundation] reject invalid method override (nicolas-grekas) - -* 4.2.6 (2019-04-16) - - * bug #31088 [DI] fix removing non-shared definition while inlining them (nicolas-grekas) - * bug #29944 [DI] Overriding services autowired by name under _defaults bind not working (przemyslaw-bogusz, renanbr) - * bug #30993 [FrameworkBundle] Fix for Controller DEPRECATED when using composer --optimized (aweelex) - * bug #31076 [HttpKernel] Fixed LoggerDataCollector crashing on empty file (althaus) - * bug #31071 property normalizer should also pass format and context to isAllowedAttribute (dbu) - * bug #31059 Show more accurate message in profiler when missing stopwatch (linaori) - * bug #31026 [Serializer] Add default object class resolver (jdecool) - * bug #31031 [Serializer] MetadataAwareNameConverter: Do not assume that property names are strings (soyuka) - * bug #31043 [VarExporter] support PHP7.4 __serialize & __unserialize (nicolas-grekas) - * bug #30423 [Security] Rework firewall's access denied rule (dimabory) - * bug #31020 [VarExporter] fix exporting classes with private constructors (nicolas-grekas) - * bug #31012 [Process] Fix missing $extraDirs when open_basedir returns (arsonik) - * bug #30852 [Console] fix buildTableRows when Colspan is use with content too long (Raulnet) - * bug #30950 [Serializer] Also validate callbacks when given in the normalizer context (dbu) - * bug #30907 [Serializer] Respect ignored attributes in cache key of normalizer (dbu) - * bug #30085 Fix TestRunner compatibility to PhpUnit 8 (alexander-schranz) - * bug #30999 Fix dark themed componnents (ro0NL) - * bug #30977 [serializer] prevent mixup in normalizer of the object to populate (dbu) - * bug #30976 [Debug] Fixed error handling when an error is already handled when another error is already handled (5) (lyrixx) - * bug #30979 Fix the configurability of CoreExtension deps in standalone usage (stof) - * bug #30918 [Cache] fix using ProxyAdapter inside TagAwareAdapter (dmaicher) - * bug #30961 [Form] fix translating file validation error message (xabbuh) - * bug #30951 Handle case where no translations were found (greg0ire) - * bug #29800 [Validator] Only traverse arrays that are cascaded into (corphi) - * bug #30921 [Translator] Warm up the translations cache in dev (tgalopin) - * bug #30922 [TwigBridge] fix horizontal spacing of inlined Bootstrap forms (xabbuh) - * bug #30860 [Profiler] Fix dark theme elements color (dFayet) - * bug #30895 [Form] turn failed file uploads into form errors (xabbuh) - * bug #30919 [Translator] Fix wrong dump for PO files (deguif) - * bug #30889 [DependencyInjection] Fix a wrong error when using a factory (Simperfit) - * bug #30911 [Console] Fix table trailing backslash (maidmaid) - * bug #30903 [Messenger] Uses the `SerializerStamp` when deserializing the envelope (sroze) - * bug #30879 [Form] Php doc fixes and cs + optimizations (Jules Pietri) - * bug #30883 [Console] Fix stty not reset when aborting in QuestionHelper::autocomplete() (Simperfit) - * bug #30878 [Console] Fix inconsistent result for choice questions in non-interactive mode (chalasr) - * bug #30825 [Routing] Fix: annotation loader ignores method's default values (voronkovich) - -* 4.2.5 (2019-04-02) - - * bug #30660 [Bridge][Twig] DebugCommand - fix escaping and filter (SpacePossum) - * bug #30784 [Translator] Add resource path to exception message for schema valida… (jschaedl) - * bug #30720 Fix getSetMethodNormalizer to correctly ignore the attributes specified in "ignored_attributes" (Emmanuel BORGES) - * bug #30749 [Serializer] Added check of constuctor modifiers to AbstractNormalizer (NekaKawaii) - * bug #30776 [Routing] Fix routes annotation loading with glob pattern (snoob) - * bug #30773 [DependencyInjection] Fix hardcoded hotPathTagName (jderusse) - * bug #30737 [Validator] Improve constraint default option check (vudaltsov) - * bug #30736 [Validator] Fix annotation default for @Count and @Length (vudaltsov) - * bug #30621 [Cache] Ensure key exists before checking array value (jrjohnson) - * bug #30711 [Serializer] Use object class resolver when extracting attributes (joelwurtz) - * bug #30641 [FrameworkBundle] properly describe service definitions without class (xabbuh) - * bug #30620 [FrameworkBundle][HttpFoundation] make session service resettable (dmaicher) - * bug #30648 Debug finalized config in debug:config (ro0NL) - * bug #30640 [Phpunit] fixed support for PHP 5.3 (fabpot) - * bug #30616 Fix case when multiple loaders are providing paths for the same namespace (yceruto) - * bug #30595 Do not validate child constraints if form has no validation groups (maryo) - * bug #30440 [TwigBridge] Fix DebugCommand when chain loader is involved (yceruto) - * bug #30479 Check if Client exists when test.client does not exist, to provide clearer exception message (SerkanYildiz) - * bug #30597 [Form] Added ResetInterface to CachingFactoryDecorator (HeahDude) - * bug #30593 Fixed usage of TranslatorInterface in form extension (fixes #30591) (althaus) - * feature #30584 [Intl] Add compile binary (ro0NL) - * bug #30487 Fix Cache error while using anonymous class (Emmanuel BORGES) - * bug #30576 [Cache] fix LockRegistry (nicolas-grekas) - * bug #30548 Correct language code for ukrainian language (stanleyk) - * bug #30518 [Cache] Fix perf when using RedisCluster by reducing roundtrips to the servers (nicolas-grekas) - * bug #30515 [Cache] Only delete one key at a time when on Predis + Cluster (andrerom) - * bug #30511 [Process] fix using argument $php of new PhpProcess() (nicolas-grekas) - * bug #30507 [Routing] Fixed XML options resolution (Jules Pietri) - * bug #30506 [TwigBridge] remove deprecation triggered when using Twig 2.7 (nicolas-grekas) - * bug #30496 [PHPUnit-Bridge] override some Composer environment variables (nicoweb) - * bug #30505 [TwigBridge] Remove usages of the spaceless tag (nicolas-grekas) - * bug #30466 [Messenger] Make 'headers' key optional for encoded messages (yceruto) - * bug #30474 compatibility with phpunit8 (garak) - * bug #30497 [HttpKernel] Change default log level for output streams (yceruto) - * bug #30498 [translation] Update defaut format from yml to yaml (GaryPEGEOT) - * bug #30490 Don't resolve the Deprecation error handler mode until a deprecation is triggered (Emmanuel BORGES) - * bug #30396 [Form] Avoid a form type extension appears many times in debug:form (markitosgv) - * bug #30361 [PropertyInfo] Fix undefined variable fromConstructor when passing context to getTypes (mantis) - * bug #30361 [PropertyInfo] Fix undefined variable fromConstructor when passing context to getTypes (mantis, OskarStark) - * bug #30410 [Monolog] Really reset logger when calling logger::reset() (lyrixx) - * bug #30437 [Debug] detect annotations before blank docblock lines (xabbuh) - * bug #30417 Autoconfig: don't automatically tag decorators (dunglas) - * bug #30392 [PropertyAccess] Fixed PropertyPathBuilder remove that fails to reset internal indexes (GregOriol) - -* 4.2.4 (2019-03-03) - - * bug #30383 [WebProfilerBundle] toolbar: invisible route name in Firefox (inmarelibero) - * bug #26532 [HttpKernel] Correctly merging cache directives in HttpCache/ResponseCacheStrategy (aschempp) - * bug #30363 Fixed the DebugClassLoader compatibility with eval()'d code on Darwin (skalpa) - * bug #30329 [Form] IntegerType: reject submitted non-integer numbers (xabbuh) - * bug #30331 [Cache] fix warming up cache.system and apcu (nicolas-grekas) - * bug #30347 [Security] Change FormAuthenticator if condition (PReimers) - * bug #30354 [Console] handles multi-byte characters in autocomplete (jls-esokia) - * bug #30351 Fix getItems() performance issue with RedisCluster (php-redis) (andrerom) - * bug #30350 [VarDumper] Keep a ref to objects to ensure their handle cannot be reused while cloning (nicolas-grekas) - * bug #30327 [HttpKernel] Fix possible infinite loop of exceptions (enumag) - * bug #27601 [Routing] fix URL generation with look-around requirements (nasimnabavi) - * bug #30277 [Console] Prevent ArgvInput::getFirstArgument() from returning an option value (chalasr) - * bug #29981 [Security] Complain about an empty decision strategy (corphi) - * bug #29822 [EventDispatcher] Fix unknown priority (ro0NL) - * bug #30324 [Validator] Fixed duplicate UUID (ralfkuehnel) - * bug #30265 [Form] do not validate non-submitted form fields in PATCH requests (xabbuh) - * bug #30313 Avoid mutating the Finder when building the iterator (stof) - * bug #30294 [FrameworkBundle] Fix Descriptor throwing on non existent parent (GuilhemN) - * bug #30271 [Console] Fix command testing with missing user inputs (chalasr) - * bug #30278 Remove unnecessary ProgressBar stdout writes (fixes flickering) (ostrolucky) - * bug #30274 [VarDumper] fix serializing Stub instances (nicolas-grekas) - * bug #30273 [Validator] Added missing use statement for UnexpectedTypeException (devrck) - * bug #30247 Don't resolve the Deprecation error handler mode until a deprecation is triggered (ossinkine) - * bug #30264 [Debug][ErrorHandler] Preserve next error handler (fancyweb) - * bug #30245 fix lost namespace in eval (fizzka) - * bug #30090 [FrameworkBundle] add constraint validators before optimizations (xabbuh) - * feature #30126 [Form] forward valid numeric values to transform() (xabbuh) - * bug #30122 [Security] fix switch user without having current token (Antoine Lamirault) - * bug #30136 use PropertyAccessorInterface instead of PropertyAccessor (nick-zh) - * bug #30124 Fix KernelTestCase compatibility for PhpUnit 8 (bis) (nicolas-grekas) - * bug #30061 [Form] render integer types with grouping as text input (xabbuh) - * bug #30063 [Form] don't lose int precision with not needed type casts (xabbuh) - * bug #30076 [Form] ignore _method forms in NativeRequestHandler (xabbuh) - * bug #30084 Fix KernelTestCase compatibility for PhpUnit 8 (alexander-schranz) - * bug #30093 [DependencyInjection] add $lazyLoad context to the generated code for lazy non-shared service by PhpDumper (XuruDragon) - * bug #30102 [Workflow] Graphviz dumper escape not always a string (Korbeil) - * bug #29884 [Form] CsrfValidationListener marks the token as invalid if it is not a string (umpirsky) - * bug #30058 [Routing] fix perf issue when dumping large number of routes (nicolas-grekas) - * bug #30062 [Form] do not overwrite the constraint being evaluated (xabbuh) - * bug #30074 Fix wrong value in file id attribute for Xliff 2.0 (deguif) - * bug #30078 [Messenger] Fix DataCollector template (ottaviano) - * bug #30087 [PhpUnitBridge] fix PHP 5.3 compat (nicolas-grekas) - -* 4.2.3 (2019-02-03) - - * bug #30050 [Cache] fix pruning pdo cache for vendors that throw on execute (bendavies) - * bug #30046 [DI] Fix dumping Doctrine-like service graphs (nicolas-grekas) - * bug #30028 [Form] fix some docblocks and type checks (xabbuh) - * bug #30037 Disable Twig in the profiler menu when Twig is not used (javiereguiluz) - * bug #30026 [VarDumper] dont implement Serializable in Stub (nicolas-grekas) - * bug #30034 [Config] ensure moving away from Serializable wont break cache:clear (nicolas-grekas) - * bug #29532 [Messenger] fixed RabbitMQ arguments not passed as integer values (thePanz) - * bug #30013 [Routing] dont redirect routes with greedy trailing vars with no explicit slash (nicolas-grekas) - * bug #30006 [Security] don't do nested calls to serialize() (nicolas-grekas, Renan) - * bug #30007 [FrameworkBundle] Support use of hyphen in asset package name (damaya, XuruDragon) - * bug #30004 Fix format strings for deprecation notices (TysonAndre) - * bug #29984 [VarDumper] Fixed search bar (ro0NL) - * bug #29764 [HttpFoundation] Check file exists before unlink (adam-mospan) - * bug #29783 [HttpFoundation] MemcachedSessionHandler::close() must close connection (grachevko) - * bug #29794 Always pass $key to NullAdapter->createCacheItem (TysonAndre) - * bug #29844 [Console] Fixed #29835: ConfirmationQuestion with default true for answer '0' (mrthehud) - * bug #29869 [Debug][ErrorHandler] Preserve our error handler when a logger sets another one (fancyweb) - * bug #29900 [Cache] PDO-based cache pool table autocreation does not work (errogaht) - * bug #29926 [Form] Changed UrlType input type to text when default_protocol is not null (MatTheCat) - * bug #29961 [Translation] Concatenated translation messages (Stadly) - * bug #29847 [Cache] fix used variable name (xabbuh) - * bug #29920 [Debug][DebugClassLoader] Match more cases for final, deprecated and internal classes / methods extends (fancyweb) - * bug #29922 Avoid dots in generated class names (derrabus) - * bug #29863 [Security] Do not mix password_*() API with libsodium one (chalasr) - * bug #29894 [DependencyInjection] the string "0" is a valid service identifier (xabbuh) - * bug #29885 Update MimeType extensions (fabpot) - * bug #29875 [TwigBridge] fix compatibility with Twig >= 2.6.1 (xabbuh) - * bug #29873 [Debug] remove return type hint for PHP 5 compatibility (xabbuh) - * bug #29837 Fix SwiftMailerHandler to support Monolog's latest reset functionality (Seldaek) - * bug #29853 Revert "bug #29597 [DI] fix reporting bindings on overriden services as unused" (mmarynich) - * bug #29833 [DebugClassLoader] expose proxyfied findFile() method (fancyweb) - -* 4.2.2 (2019-01-06) - - * bug #29494 [HttpFoundation] Fix request uri when it starts with double slashes (alquerci) - * bug #29697 [DI] Fixed wrong factory method in exception (Wojciech Gorczyca) - * bug #29679 [HttpKernel] Correctly Render Signed URIs Containing Fragments (zanbaldwin) - * bug #29754 Ensure final input of CommandTester works with default (Firehed) - * bug #29695 [Form] Do not ignore the choice groups for caching (vudaltsov) - * bug #29738 [Intl] handle null date and time types (xabbuh) - * bug #29708 [FrameworkBundle] access the container getting it from the kernel (xabbuh) - * bug #29676 [HttpFoundation] Fix erasing cookies issue (eiannone) - * bug #29741 [VarExporter] fix exporting array indexes (xabbuh) - * bug #29704 [FrameworkBundle] improve errors in tests missing the BrowserKit component (xabbuh) - * bug #29721 [SecurityBundle] Fix traceable voters (ro0NL) - * bug #29617 [Console] Add specific replacement for help text in single command applications (codedmonkey) - * bug #29714 [Event Dispatcher] fixed 29703: TraceableEventDispatcher reset() callStack to null (mlievertz) - * bug #29597 [DI] fix reporting bindings on overriden services as unused (nicolas-grekas) - * bug #29639 [Yaml] detect circular references (xabbuh) - * bug #29644 [Cache] fix bad optim (nicolas-grekas) - * bug #29648 [Cache] fix Simple\Psr6Cache proxying of metadata (nicolas-grekas) - * bug #29569 [FrameworkBundle] decouple debug:autowiring from phpdocumentor/reflection-docblock (SerkanYildiz) - * bug #29546 [DI] map snake-case ids of service subscribers to camel-case autowiring aliases (nicolas-grekas) - * bug #29409 Fix env fallback to an unresolved variable (jderusse) - * bug #29626 [Routing] fix trailing slash redirections involving a trailing var (nicolas-grekas) - * bug #29411 [EventDispatcher] Revers event tracing order (ro0NL) - * bug #29533 Fixed public directory when configured in composer.json (alexander-schranz) - * bug #29619 [Console] OutputFormatter: move strtolower to createStyleFromString (ogizanagi) - * bug #29621 [Security] Prefer clone() over unserialize(serialize()) for user refreshment (chalasr) - * bug #29591 [Cache] Fix undefined variable in ArrayTrait (eXtreme) - * bug #29558 [Messenger] Restore message handlers laziness (chalasr) - * bug #29589 [VarExporter] dont call userland code with uninitialized objects (nicolas-grekas) - * bug #29542 [Routing] fix dumping same-path routes with placeholders (nicolas-grekas) - * bug #29587 [Debug] ignore underscore vs backslash namespaces in DebugClassLoader (nicolas-grekas) - * bug #29584 [FrameworkBundle] fix describing routes with no controllers (nicolas-grekas) - * bug #29582 [DI] move RegisterServiceSubscribersPass before DecoratorServicePass (kbond) - * bug #29527 [TwigBridge][Form] Prevent multiple rendering of form collection prototypes (Shoplifter) - * bug #29571 [Yaml] ensures that the mb_internal_encoding is reset to its initial value (Jörn Lang) - * bug #29513 [Hackday][Serializer] Deserialization ignores argument type hint from phpdoc for array in constructor argument (karser) - * bug #29323 [Security] defer log message in guard authenticator (eschultz-magix) - * bug #29539 [WebProfilerBundle][TwigBundle] CSS fixes (ro0NL) - * bug #29543 [Cache] Don't erase processed redis dsn (chalasr) - * bug #29531 [Validator] Added IBAN format for Vatican City State (raulfraile) - * bug #29501 [Form] filter out invalid language values (xabbuh) - * bug #29307 [Form] Filter arrays out of scalar form types (nicolas-grekas) - * bug #29500 [Form] filter out invalid Intl values (xabbuh) - * bug #29499 [Validator] Fixed grouped composite constraints (HeahDude) - -* 4.2.1 (2018-12-06) - - * security #cve-2018-19790 [Security\Http] detect bad redirect targets using backslashes (xabbuh) - * security #cve-2018-19789 [Form] Filter file uploads out of regular form types (nicolas-grekas) - * bug #29481 [TwigBridge] Deprecating legacy Twig paths in DebugCommand and simplifications (yceruto) - * bug #29436 [Cache] Fixed Memcached adapter doClear()to call flush() (raitocz) - * bug #29482 Fixes sprintf(): Too few arguments in MessageFormatter::choiceFormat (stephanedelprat) - * bug #29461 [Contracts] extract LocaleAwareInterface out of TranslatorInterface (nicolas-grekas) - * bug #29446 [VarExporter] fix dumping private properties from abstract classes (nicolas-grekas) - * bug #29441 [Routing] ignore trailing slash for non-GET requests (nicolas-grekas) - * bug #29445 [FrameworkBundle] Fix empty output for debug:autowiring when reflection-docblock is not installed (chalasr) - * bug #29444 [Workflow] Fixed BC break for Workflow metadata (lyrixx) - * bug #29432 [DI] dont inline when lazy edges are found (nicolas-grekas) - * bug #29413 [Serializer] fixed DateTimeNormalizer to maintain microseconds when a different timezone required (rvitaliy) - * bug #29424 [Routing] fix taking verb into account when redirecting (nicolas-grekas) - * bug #29418 [VarExporter] fix dumping protected property from abstract classes (nicolas-grekas) - * bug #29414 [DI] Fix dumping expressions accessing single-use private services (chalasr) - * bug #28853 [LDAP] Add TIMEOUT Option to LDAP Connection Options (lmatte7) - * bug #29399 [FrameworkBundle] define doctrine as default_pdo_provider only if the package is installed (nicolas-grekas) - * bug #29375 [Validator] Allow `ConstraintViolation::__toString()` to expose codes that are not null or emtpy strings (phansys) - * bug #29376 [EventDispatcher] Fix eventListener wrapper loop in TraceableEventDispatcher (jderusse) - * bug #29386 undeprecate the single-colon notation for controllers (fbourigault) - * bug #29393 [DI] fix edge case in InlineServiceDefinitionsPass (nicolas-grekas) - * bug #29394 [Config] fix path exclusion during glob discovery (nicolas-grekas) - * bug #29395 [FrameworkBundle][Messenger] Restore check for messenger serializer default id (ogizanagi) - * bug #29380 [Routing] fix greediness of trailing slash (nicolas-grekas) - -* 4.2.0 (2018-11-30) - - * bug #29343 [Form] Handle all case variants of "nan" when parsing a number (mwhudson, xabbuh) - * bug #29373 [Routing] fix trailing slash redirection (nicolas-grekas) - * bug #29355 [PropertyAccess] calculate cache keys for property setters depending on the value (xabbuh) - * bug #29369 [DI] fix combinatorial explosion when analyzing the service graph (nicolas-grekas) - * bug #29349 [Debug] workaround opcache bug mutating "$this" !?! (nicolas-grekas) - * bug #29344 Fixes sprintf(): Too few arguments in Translator (stephanedelprat) - * bug #29318 [Console] Move back root exception to stack trace in verbose mode (chalasr) - -* 4.2.0-RC1 (2018-11-26) - - * bug #29332 [PropertyAccess] make cache keys encoding bijective (nicolas-grekas) - * bug #29298 [Routing] fix trailing slash redirection when using RedirectableUrlMatcher (nicolas-grekas) - * bug #29297 [Routing] fix trailing slash redirection when using RedirectableUrlMatcher (nicolas-grekas) - * bug #29313 [PropertyAccessor] fix encoding of cache keys (nicolas-grekas) - * bug #29328 [HttpKernel] handle anonymous classes when generating the dumped container class name (nicolas-grekas) - * bug #28917 [DoctrineBridge] catch errors while converting to db values in data collector (alekitto) - * bug #29317 [WebProfiler] Detect non-file paths in file viewer (ro0NL) - * bug #29305 [EventDispatcher] Unwrap wrapped listeners internally (ro0NL) - * bug #29302 [Contracts][Cache] allow retrieving metadata of cached items (nicolas-grekas) - * bug #29315 [DI] fix copying expression providers when analyzing the service graph (nicolas-grekas) - * bug #27314 [DoctrineBridge] fix case sensitivity issue in RememberMe\DoctrineTokenProvider (PF4Public) - * bug #29310 [MonologBridge] Return empty list for unknown requests (ro0NL) - * bug #29316 [VarDumper] Fix ClassStub ellipsis (ro0NL) - * bug #29300 [Translation] fix dumping catalogues cache (nicolas-grekas) - * bug #29308 [Translation] Use XLIFF source rather than resname when there's no target (thewilkybarkid) - * bug #26244 [BrowserKit] fixed BC Break for HTTP_HOST header (brizzz) - * bug #28147 [DomCrawler] exclude fields inside "template" tags (Gorjunov) - * bug #29260 [Lock] Fixed PdoStore::putOffExpiration(), PdoStore::getHashedKey() (PavelPrischepa) - * bug #29222 [Dotenv] properly parse backslashes in unquoted env vars (xabbuh) - * bug #29256 [HttpFoundation] Fixed absolute Request URI with default port (thomasbisignani) - * bug #29274 [Routing] Remove duplicate schemes and methods for invokable controllers (claudusd) - * bug #29285 [HttpKernel][WebProfilerBundle] Getting the cached client mime type instead of guessing it again (yceruto) - * bug #29271 [HttpFoundation] Fix trailing space for mime-type with parameters (Sascha Dens) - * feature #29167 [Messenger] Add a trait for synchronous query & command buses (ogizanagi) - * bug #29243 [Cache] fix optimizing Psr6Cache for AdapterInterface pools (nicolas-grekas) - * bug #29247 [DI] fix taking lazy services into account when dumping the container (nicolas-grekas) - * bug #29249 [Form] Fixed empty data for compound date interval (HeahDude) - * bug #29265 [Bridge/PhpUnit] Use composer to download phpunit (nicolas-grekas) - * bug #28769 [FrameworkBundle] deal with explicitly enabled workflow nodes (xabbuh) - -* 4.2.0-BETA2 (2018-11-16) - - * bug #29190 [Debug][HttpKernel] remove frames added by DebugClassLoader in stack traces (nicolas-grekas) - * bug #29233 [FrameworkBundle] metadata_update_threshold default value must be an int (dunglas) - * bug #29226 [Messenger] Improved message when handler class does not exist (neeckeloo) - * bug #29223 [Validator] Added the missing constraints instance checks (thomasbisignani) - * bug #28966 [PropertyAccessor] Fix unable to write to singular property using setter while plural adder/remover exist (karser) - * bug #29182 [Form] Fixed empty data for compound date types (HeahDude) - * bug #29224 [SecurityBundle] Fix remember-me cookie framework inheritance when session is disabled (fbourigault) - * bug #29220 [Translation] make intl+icu format seamless by handling it in MessageCatalogue (nicolas-grekas) - * feature #29166 [Messenger] Add handled & sent stamps (ogizanagi) - * bug #29209 [VarExporter] fix handling of __sleep() (nicolas-grekas) - * bug #29196 [Messenger] Fix collecting messages (ro0NL) - * bug #29205 [Dotenv] skip loading "local" env twice (nicolas-grekas) - * bug #29204 [FrameworkBundle][WebServerBundle] Revert deprecation of --env and --no-debug console options (chalasr) - * bug #29191 [Routing] generate(null) should throw an exception (nicolas-grekas) - * bug #29199 [FrameworkBundle] conflict with Dotenv <4.2 to simplify recipes (nicolas-grekas) - * bug #29197 Revert "bug #29154 [FrameworkBundle] Define APP_ENV/APP_DEBUG from argv via Application::bootstrapEnv() (nicolas-grekas) - * bug #29185 [Form] Fixed keeping hash of equal \DateTimeInterface on submit (HeahDude) - * bug #29183 [HttpKernel] Fix collecting uploaded files (ro0NL) - * bug #29141 [Workflow] Fixed bug of buildTransitionBlockerList when many transition are enabled (Tetragramat, lyrixx) - * bug #29137 [Workflow][FrameworkBundle] fixed guard event names for transitions (destillat, lyrixx) - * bug #29184 [WebProfilerBundle] Fix theme settings (ro0NL) - * feature #29159 [Messenger] collect all stamps added on Envelope as collections (nicolas-grekas) - * bug #29171 [Dotenv] load .env.dist when it exists and .env is not found (nicolas-grekas) - * bug #28731 [Form] invalidate forms on transformation failures (xabbuh) - * bug #29152 [Config] Unset key during normalization (ro0NL) - * bug #29165 [DI] align IniFileLoader to PHP bugfix #76965 (nicolas-grekas) - * bug #29154 [FrameworkBundle] Define APP_ENV/APP_DEBUG from argv via Application::bootstrapEnv() (chalasr) - * bug #29129 [Dotenv] add loadEnv(), a smoother alternative to loadForEnv() (nicolas-grekas) - * bug #29113 [Routing] fix dumping conditions that use the request (nicolas-grekas) - * bug #29115 Change button_widget class to btn-primary (neFAST) - * bug #29131 [Dotenv] dont use getenv() to read SYMFONY_DOTENV_VARS (nicolas-grekas) - * bug #29057 [HttpFoundation] replace any preexisting Content-Type headers (nicolas-grekas) - * bug #29076 [Serializer] Allow null values when denormalizing with constructor missing data (danut007ro) - * bug #29128 [FrameworkBundle] Cleaning translation commands and fix a bug for --all option (yceruto) - * bug #29104 [DI] fix dumping inlined services (nicolas-grekas) - * bug #29054 [VarDumper] fix dump of closures created from callables (nicolas-grekas) - * bug #29102 [DI] fix GraphvizDumper ignoring inline definitions (nicolas-grekas) - * bug #29090 LoggingTranslator should implement Symfony\Contracts\Translation\TranslatorInterface (desmax) - * bug #29095 [TwigBridge] require the needed symfony/contracts package (xabbuh) - * bug #29107 [DI] dont track classes/interfaces used to compute autowiring error messages (nicolas-grekas) - * bug #29094 Add samesite attribute to session cookie after session migration (rpkamp) - * bug #29080 [FrameworkBundle] fix deps (ro0NL) - -* 4.2.0-BETA1 (2018-11-03) - - * feature #28622 [VarDumper] add caster for Memcached (jschaedl) - * feature #29042 [DI] use filter_var() instead of XmlUtils::phpize() in EnvVarProcessor (nicolas-grekas) - * feature #29047 Revert "[HttpFoundation] Adds getAcceptableFormats() method for Request" (Tobion) - * feature #29046 [Bridge/Doctrine] remove workarounds from the past (nicolas-grekas) - * feature #29022 [Cache] allow to skip saving the computed value when using CacheInterface::get() (nicolas-grekas) - * feature #29010 [Messenger] make senders and handlers subscribing to parent interfaces receive *all* matching messages, wildcard included (nicolas-grekas) - * feature #29006 [Messenger] make TraceableMiddleware decorate a StackInterface instead of each middleware to free the callstack from noisy frames (nicolas-grekas) - * feature #28970 [FrameworkBundle] make debug:autowiring list useful services and their description (nicolas-grekas) - * feature #28952 [Translation] allow using the ICU message format using domains with the "+intl-icu" suffix (nicolas-grekas) - * feature #27914 [Security][SecurityBundle] Add voter individual decisions to profiler (l-vo) - * feature #28985 [Messenger] Move MiddlewareTestCase in Test ns (ogizanagi) - * feature #28892 [FrameworkBundle] Deprecate support for legacy directories in Translation comands (chalasr) - * feature #28854 [VarDumper] Scroll into view when searching (ro0NL) - * feature #28997 [FrameworkBundle] Deprecating support for legacy translations directory (yceruto) - * feature #28983 [Messenger] make dispatch(), handle() and send() methods return Envelope (nicolas-grekas) - * feature #28533 [DotEnv] Add a new loadForEnv() method mimicking Ruby's dotenv behavior (dunglas) - * feature #28943 [Messenger] Add `StackInterface`, allowing to unstack the call stack (nicolas-grekas) - * feature #28860 [Form] Deprecate TimezoneType regions option (ro0NL) - * feature #28945 [Messenger] remove AllowNoHandlerMiddleware in favor of a constructor argument on HandleMessageMiddleware (nicolas-grekas) - * feature #28947 [Messenger] remove classifying sub-namespaces in favor of semantic ones (nicolas-grekas) - * feature #27917 [Validator] catch any UnexpectedValueException on validation (xabbuh) - * feature #28875 [FWBundle] Add a new method AbstractController::addLink() (dunglas) - * feature #28934 [WebProfilerBundle] Add channel log filter (ro0NL) - * feature #28939 [WebProfilerBundle] Remove application name (ro0NL) - * feature #28709 [Serializer] Refactor and uniformize the config by introducing a default context (dunglas) - * feature #28914 [Messenger] make Envelope first class citizen for middleware handlers (nicolas-grekas) - * feature #28909 [Messenger] made dispatch() and handle() return void (nicolas-grekas) - * feature #28936 [WebProfilerBundle] Replay referer URL (ro0NL) - * feature #28893 [TwigBundle] Fix usage of TwigBundle without FrameworkBundle (tgalopin) - * feature #28891 [TwigBundle] Deprecating support for legacy templates directories (yceruto) - * feature #28911 [Messenger] rename "envelope items" and move them in the "Stamp" namespace (nicolas-grekas) - * feature #27043 [Form][TwigBridge] Add help_attr (mpiot) - * feature #28810 [HttpKernel] Deprecate usage of getRootDir() and kernel.root_dir (fabpot) - * feature #28809 [HttpKernel] Deprecate the Kernel name (fabpot) - * feature #28807 [HttpFoundation] Make ResponseHeaderBag::makeDisposition static (fabpot) - * feature #28842 [Validator] Deprecate checkMX and checkHost on Email validator (fabpot) - * feature #28833 [Intl] Blacklist invalid languages (ro0NL) - * feature #28815 YamlEncoder handle yml format (kevin-biig) - * feature #27742 [Process] Add feature "wait until callback" to process class (Nek-) - * feature #28713 [Cache] added support for connecting to Redis clusters via DSN (nicolas-grekas) - * feature #24263 Filter logs by level (ro0NL) - * feature #24151 Display the log context in the debug pages (javiereguiluz) - * feature #26261 [Validator] Improvement: provide file basename for constr. violation messages in FileValidator. (TheCelavi) - * feature #26324 [Form] allow additional http methods in form configuration (alekitto) - * feature #26771 [Filesystem] Fix mirroring a directory with a relative path and a custom iterator (fxbt) - * feature #27291 [OptionsResolver] Added support for nesting options definition (yceruto) - * feature #27261 [VarDumper] Allow to use a light theme out of the box (ogizanagi) - * feature #27967 [Finder] Added a way to inverse a previous sorting (lyrixx) - * feature #28061 [Security] add port in access_control (roukmoute) - * feature #28476 Added different protocols to be allowed as asset base_url (alexander-schranz) - * feature #27770 [FrameworkBundle] Moving Cache-related CompilerPass to Cache component (Korbeil) - * feature #28738 [OptionsResolver] Passing Options argument to deprecation closure (yceruto) - * feature #28718 [Cache] add CacheInterface::delete() + improve CacheTrait (nicolas-grekas) - * feature #24530 [Form] simplify the form type extension registration (xabbuh) - * feature #28586 [WebServerBundle] Added ability to display the current hostname address if available when binding to 0.0.0.0 (respinoza) - * feature #28763 [WebProfilerBundle] Extract server parameters into their own tab (fabpot) - * feature #28375 [Translator] Deprecated transChoice and moved it away from contracts (Nyholm, nicolas-grekas) - * feature #28745 [WebServerBundle] Deprecate relying on --env in server:start and server:run (chalasr) - * feature #28505 [Serialized] allow configuring the serialized name of properties through metadata (fbourigault) - * feature #28669 [Serializer] Object class resolver (alanpoulain) - * feature #28653 [FrameworkBundle] Deprecate the "--env" and "--no-debug" console options (chalasr) - * feature #28693 [Security] Deprecate simple_preauth and simple_form in favor of Guard (chalasr) - * feature #28626 [Translation] marked getFallbackLocales() as internal (boscho87) - * feature #28571 [DependencyInjection] Improve ServiceLocatorTagPass service matching (codedmonkey) - * feature #28644 [Validator] Pre-check constraint validator dependencies (ro0NL) - * feature #28661 [Serializer] Add an option to skip null values (dunglas) - * feature #28679 [FrameworkBundle] Add vscode editor to ide config (lexcast) - * feature #28656 When a CSRF occures on a Form submit add a cause on the FormError object (gmponos) - * feature #28588 [Cache] add "setCallbackWrapper()" on adapters implementing CacheInterface for more flexibility (nicolas-grekas) - * feature #28598 [Cache] support configuring multiple Memcached servers in one DSN (nicolas-grekas) - * feature #28447 [HttpFoundation] make cookies auto-secure when passing them $secure=null + plan to make it and samesite=lax the defaults in 5.0 (nicolas-grekas) - * feature #28446 [SecurityBundle] make remember-me cookies auto-secure + inherit their default config from framework.session.cookie_* (nicolas-grekas) - * feature #28417 [VarExporter] add Instantiator::instantiate() to create+populate objects without calling their constructor nor any other methods (nicolas-grekas) - * feature #27819 [Serializer] deprecated normalizers and encoders who dont implement the base interfaces (rodnaph) - * feature #28572 Make it clear that the profiler is for dev only (fabpot) - * feature #28536 Favor LogicException for missing classes & functions (ro0NL) - * feature #28569 [Form] deprecate precision in IntegerToLocalizedStringTransformer (xabbuh) - * feature #28570 [Form] deprecate the unused scale option (xabbuh) - * feature #28566 [VarDumper] add casters for IntlDateFormatter and IntlCalendar (jschaedl) - * feature #28559 [VarDumper] add caster for IntlTimeZone (jschaedl) - * feature #28449 [DependencyInjection] improved message when alias service is not found (xabbuh) - * feature #27434 [Console] Add support for error ouput in the CommandTester (cdekok) - * feature #28555 [VarDumper] add caster for NumberFormatter (jschaedl) - * feature #28538 [Lock] Wrap release exception (jderusse) - * feature #28551 [VarDumper] add caster for MessageFormatter (nicolas-grekas) - * feature #28329 [Debug] Trigger a deprecation for new parameters not defined in sub classes (GuilhemN) - * feature #27920 Add Zookeeper data store for Lock Component (Ganesh Chandrasekaran) - * feature #28317 [VarDumper] Allow dd() to be called without arguments (SjorsO) - * feature #28424 [Ldap] Add verbose ext-ldap error if present for easier debugging (scaytrase) - * feature #28521 [Yaml] Added support for multiple files or directories in LintCommand (yceruto) - * feature #28522 [Translation] Added support for multiple files or directories in XliffLintCommand (yceruto) - * feature #28523 [FrameworkBundle] Register an identity translator as fallback (yceruto) - * feature #28473 [Validator] Check the BIC country with symfony/intl (sylfabre) - * feature #28487 [FrameworkBundle] Ignore backslashes in service ids when using debug:container and debug:autowiring (respinoza) - * feature #28412 [PhpUnitBridge] enable DebugClassLoader by default (nicolas-grekas) - * feature #28416 [FrameworkBundle] bind "ContainerInterface $parameterBag" arguments to the "parameter_bag" service (nicolas-grekas) - * feature #28316 Trigger deprecation notices when inherited class calls parent method but misses adding new arguments (kevinjhappy) - * feature #28373 [Console] Support max column width in Table (ro0NL) - * feature #28422 [VarExporter] throw component-specific exceptions (nicolas-grekas) - * feature #28415 [FrameworkBundle] Deprecate ContainerAwareCommand (chalasr) - * feature #28419 [Messenger] Change AmqpExt classes constructor signature (fabpot) - * feature #28405 [Messenger] Uses a messenger serializer, not an individual encoder/decoder (sroze) - * feature #28298 [WebServerBundle] Add support for Xdebug's Profiler (maidmaid) - * feature #28399 [Messenger] Add a SenderLocator decoupled from ContainerInterface (fabpot) - * feature #27321 [Messenger][Profiler] Trace middleware execution (ogizanagi) - * feature #28400 [Messenger] Add a simple serializer (fabpot) - * feature #28397 [Messenger] Change exceptions to use component's one (fabpot) - * feature #28387 [HttpKernel][Profiler] Add arg value resolver category in performances panel (ogizanagi) - * feature #25015 [Validator] Deprecate validating DateTimeInterface in Date|Time|DateTime constraints (ro0NL) - * feature #28394 [Messenger] Add interfaces to be type-hinted even when not using a Container (fabpot) - * feature #27981 [TwigBridge] Added template "name" argument to debug:twig command to find their paths (yceruto) - * feature #28207 [DI] leverage Contracts\Service (nicolas-grekas) - * feature #22225 [Console] Support formatted text cutting (ro0NL) - * feature #28206 [Contracts] Add traits+interfaces from the DI component (nicolas-grekas) - * feature #27456 [LOCK] Add a PdoStore (jderusse) - * feature #24297 Feature/doctrine type guesser simple json array support (iluuu1994) - * feature #26859 [Dotenv] add a flag to allow env vars override (fmata) - * feature #26997 [PropertyInfo] Add an extractor to guess if a property is initializable (dunglas) - * feature #27667 [Form][OptionsResolver] Show deprecated options definition on debug:form command (yceruto) - * feature #27021 [Serializer] Allow to access extra infos in name converters (dunglas) - * feature #26923 [FrameworkBundle] Allow user to specify folder for flock (MaksSlesarenko) - * feature #25125 [VarDumper] New env var to select the dump format (dunglas) - * feature #28117 [FrameworkBundle] add class description to debug:container command (gimler) - * feature #28270 [Messenger] Uses Symfony Serializer by default for envelope items (sroze) - * feature #27935 [FrameworkBundle] [Command] TranslationUpdate change default output to xlf (Alexis BOYER) - * feature #28168 Add SameSite cookies to FrameWorkBundle (rpkamp) - * feature #28303 [Process] Add relative path support for PHP_BINARY env var of PhpExecutableFinder (maidmaid) - * feature #28096 [Contracts] Add Cache contract to extend PSR-6 with tag invalidation, callback-based computation and stampede protection (nicolas-grekas) - * feature #27399 [Translation] Added intl message formatter. (aitboudad, Nyholm) - * feature #28315 [DI] Trigger exception when using '@id' name in parent option (Seb33300) - * feature #28210 [Contracts] Add Translation\TranslatorInterface + decouple symfony/validator from symfony/translation (nicolas-grekas) - * feature #28331 [FrameworkBundle] Don't populate fallback cache on warmup (nicolas-grekas) - * feature #28264 [VarDumper] make RedisCaster handle RedisCluster and dump all options on all drivers (nicolas-grekas) - * feature #28289 [Serializer] Add support for ignoring comments while XML encoding (maidmaid) - * feature #28294 [Messenger] Remove the "obscure" message subscriber configuration (sroze) - * feature #28271 [Messenger] Allow interfaces to be type-hinted as well (sroze) - * feature #28190 [Messenger] Add a --bus option to the messenger:consume-messages command (chalasr, sroze) - * feature #28275 [Messenger] Only subscribe to a given bus from the MessageSubscriber (sroze) - * feature #28243 [FrameworkBundle] Deprecate `Symfony\Bundle\FrameworkBundle\Controller\Controller` (sroze) - * feature #28070 [Translator] Use ICU parent locales as fallback locales (thewilkybarkid) - * feature #28231 [VarExporter] a new component to serialize values to plain PHP code (nicolas-grekas) - * feature #28244 [FrameworkBundle] Added new "auto" mode for `framework.session.cookie_secure` to turn it on when https is used (nicolas-grekas) - * feature #28277 [Serializer] AbstractObjectNormalizer improve performance (martiis) - * feature #28247 [Messenger] Don't make EnvelopeItemInterface extend Serializable (nicolas-grekas) - * feature #27926 [Serializer] XmlEncoder doesn't ignore PI nodes while encoding (maidmaid) - * feature #27890 Mock date() in ClockMock (Dominic Tubach) - * feature #28218 Improve support for anonymous classes (nicolas-grekas) - * feature #28221 [DomCrawler] Add a way to filter direct children (Einenlum) - * feature #28234 [DI] Allow autowiring by type + parameter name (nicolas-grekas) - * feature #28156 [Serializer] Fix the XML comments encoding (maidmaid) - * feature #28069 [Validator] New `DivisibleBy` constraint for testing divisibility (colinodell) - * feature #28176 [DI] [FrameworkBundle] Add LoggerAwareInterface to auto configuration (GaryPEGEOT) - * feature #27957 [Routing] Add fallback to cultureless locale for internationalized routes (fancyweb) - * feature #28027 [Config] Rename FileLoaderLoadException to LoaderLoadException (ProgMiner) - * feature #28085 [Config] show proposals when unsupported option is provided (fmata) - * feature #27806 [DI] Allow autoconfiguring bindings (nicolas-grekas) - * feature #21002 [Form] Added options for separate date/time labels in DateTimeType. (mktcode) - * feature #27763 [WebProfilerBundle] Append new ajax request to the end of the list (BoShurik) - * feature #28035 [DomCrawler] Allow using non-absolute base URIs (javiereguiluz) - * feature #28106 [Yaml] save preg_match() calls when possible (xabbuh) - * feature #27678 Allow to configure some options of the profiler interface (javiereguiluz) - * feature #27943 [Security] Deprecate returning stringish objects from Security::getUser (ro0NL) - * feature #27956 Added types and tweaked PHPdoc of clickLink() and submitForm() methods (javiereguiluz) - * feature #27976 [Security] Remember me: allow to set the samesite cookie flag (dunglas) - * feature #27978 [WebProfilerBundle] Show relative path of the template and improving panel view (yceruto) - * feature #27891 [Finder] Allow arrays as parameters of some methods for better fluent experience and code readability (jfredon) - * feature #27829 [DoctrineBridge] Inject the entity manager instead of the class metadata factory in DoctrineExtractor (dunglas) - * feature #27093 Add symfony/contracts: a set of abstractions extracted out of the Symfony components (nicolas-grekas) - * feature #27807 Added new methods submitForm and clickLink to Client class (nowiko) - * feature #27879 [Routing] deprecate non string requirement names (xabbuh) - * feature #26933 [Console] Add title table (maidmaid) - * feature #27697 [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface" (nicolas-grekas) - * feature #27645 [Cache] Add `MarshallerInterface` allowing to change the serializer, providing a default one that automatically uses igbinary when available (nicolas-grekas) - * feature #27694 [FrameworkBundle][Cache] Allow configuring PDO-based cache pools, with table auto-creation on first use (nicolas-grekas) - * feature #27774 [FrameworkBundle] allow turning routes to utf8 mode by default (nicolas-grekas) - * feature #27821 [Process][Console] deprecated defining commands as strings (nicolas-grekas) - * feature #27320 [Messenger] Activation middleware decorator (ogizanagi) - * feature #27519 [HttpKernel][FrameworkBundle] Turn HTTP exceptions to HTTP status codes by default (nicolas-grekas) - * feature #27020 [Serializer] Allow to access to the format and context in circular ref handler (dunglas) - * feature #27783 [DI] Add ServiceLocatorArgument to generate array-based locators optimized for OPcache shared memory (nicolas-grekas) - * feature #27850 [Security] Allow passing null as $filter in LdapUserProvider to get the default filter (louhde) - * feature #27650 [SecurityBundle] Add json login ldap (Rudy Onfroy) - * feature #27798 [Security] Use AuthenticationTrustResolver in SimplePreAuthenticationListener (nicolas-grekas) - * feature #27801 [MonologBridge] Add ProcessorInterface, enabling autoconfiguration of monolog processors (nicolas-grekas) - * feature #27503 [Serializer] Allow to pass a single value for the groups opt (dunglas) - * feature #27715 [Serializer] Deprecate CsvEncoder as_collection false default value (ogizanagi) - * feature #27768 [VarDumper] display the signature of callables (nicolas-grekas) - * feature #27766 [VarDumper] show proxified class on hover (nicolas-grekas) - * feature #27675 [DoctrineBridge] always load event listeners lazy via ServiceLocator (dmaicher) - * feature #27499 Improved an error message related to controllers (javiereguiluz) - * feature #26300 [PropertyInfo] Implement "Collection" types in PhpDocExtractor (popy-dev) - * feature #26946 [WebProfilerBundle] Display uploaded files in the profiler (javiereguiluz) - * feature #27476 [Config] deprecate tree builders without root nodes (xabbuh) - * feature #27586 [PropertyAccess] Add Property Path to Exception Message (rodnaph) - * feature #27699 Redesigned the default error page in production (javiereguiluz) - * feature #27655 [Translation] Added support for translation files with other filename patterns (javiereguiluz) - * feature #27580 [Form] Add ability to clear form errors (colinodell) - * feature #27247 [Form] Deprecate `searchAndRenderBlock` returning empty string (ostrolucky) - * feature #27646 [Cache] added support for phpredis 4 `compression` and `tcp_keepalive` options (nicolas-grekas) - * feature #27605 [DX] Log potential redirect loops caused by forced HTTPS (colinodell) - * feature #27653 [Translation] Improved the performance of the lint:xliff command (javiereguiluz) - * feature #27421 CacheWarmerAggregate handle deprecations logs (ScullWM) - * feature #27611 [FrameworkBundle][SecurityBundle] Moved security expression providers pass logic to SecurityBundle (HeahDude) - * feature #27277 [OptionsResolver] Introduce ability to deprecate options, allowed types and values (yceruto) - * feature #26919 [TwigBridge] Added bundle name suggestion on wrongly overrided templates paths (pmontoya, Pascal Montoya) - * feature #26486 [HttpFoundation] Adds getAcceptableFormats() method for Request (AndreiIgna) - * feature #27535 [TwigBundle] Enhance the twig not found exception (behnoushnorouzi) - * feature #27551 [FrameworkBundle] show public/private for aliases in debug:container command (OskarStark) - * feature #27543 [Cache] serialize objects using native arrays when possible (nicolas-grekas) - * feature #27563 [Cache] Improve perf of array-based pools (nicolas-grekas) - * feature #27604 [Cache] Prevent stampede at warmup using flock() (nicolas-grekas) - * feature #27315 [TwigBundle] add exception chain breadcrumbs navigation (kiler129) - * feature #27031 [Cache] Use sub-second accuracy for internal expiry calculations (nicolas-grekas) - * feature #27549 [Cache] Unconditionally use PhpFilesAdapter for system pools (nicolas-grekas) - * feature #27009 [Cache] Add stampede protection via probabilistic early expiration (nicolas-grekas) - * feature #27471 [DI] Improve performance of removing/inlining passes (nicolas-grekas) - * feature #27462 [FrameworkBundle] Deprecate auto-injection of the container in AbstractController instances (nicolas-grekas) - * feature #27077 [DependencyInjection] add ServiceSubscriberTrait (kbond) - * feature #27398 [Cache] Remove TaggableCacheInterface, alias cache.app.taggable to CacheInterface (nicolas-grekas) - * feature #27343 [Messenger][Profiler] Show dispatch caller (ogizanagi) - * feature #27429 [PropertyInfo] Auto-enable PropertyInfo component (sroze) - * feature #27430 [PropertyInfo] Add an alias to the property info type extractor (sroze) - * feature #27417 [WebProfilerBundle] Make Twig bundle an explicit dependency (fabpot) - * feature #27024 [Finder] added "use natural sort" option (vyshkant) - * feature #26934 [FrameworkBundle] Allow configuring taggable cache pools (nicolas-grekas) - * feature #26981 No more support for custom anon/remember tokens based on FQCN (Iltar van der Berg) - * feature #27336 [Security][SecurityBundle] FirewallMap/FirewallContext deprecations (chalasr) - * feature #27157 [DI] Select specific key from an array resolved env var (bobvandevijver) - * feature #27165 [DI] Allow binding by type+name (nicolas-grekas) - * feature #26929 [Cache] Add [Taggable]CacheInterface, the easiest way to use a cache (nicolas-grekas) - * feature #27305 [Security/Core] Add "is_granted()" to security expressions, deprecate "has_role()" (nicolas-grekas) - * feature #27069 [LDAP] Add "applyOperations" method to EntryManager (mablae) - * feature #27118 [BrowserKit] Adds support for meta refresh (jhedstrom) - * feature #27268 [DI] fine tune dumped factories (nicolas-grekas) - * feature #27075 [DI][DX] Allow exclude to be an array of patterns (magnetik) - * feature #27138 [HttpKernel] Better exception page when the controller returns nothing (lyrixx) - diff --git a/CHANGELOG-4.3.md b/CHANGELOG-4.3.md deleted file mode 100644 index 9c95e7e327a31..0000000000000 --- a/CHANGELOG-4.3.md +++ /dev/null @@ -1,848 +0,0 @@ -CHANGELOG for 4.3.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 4.3 minor versions. - -To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash -To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.3.0...v4.3.1 - -* 4.3.10 (2020-01-21) - - * bug #35364 [Yaml] Throw on unquoted exclamation mark (fancyweb) - * bug #35065 [Security] Use supportsClass in addition to UnsupportedUserException (linaori) - * bug #35343 [Security] Fix RememberMe with null password (jderusse) - * bug #34223 [DI] Suggest typed argument when binding fails with untyped argument (gudfar) - * bug #35324 [HttpClient] Fix strict parsing of response status codes (Armando-Walmeric) - * bug #35318 [Yaml] fix PHP const mapping keys using the inline notation (xabbuh) - * bug #35304 [HttpKernel] Fix that no-cache MUST revalidate with the origin (mpdude) - * bug #35299 Avoid `stale-if-error` in FrameworkBundle's HttpCache if kernel.debug = true (mpdude) - * bug #35151 [DI] deferred exceptions in ResolveParameterPlaceHoldersPass (Islam93) - * bug #35278 [EventDispatcher] expand listener in place (xabbuh) - * bug #35254 [PHPUnit-Bridge] Fail-fast in simple-phpunit if one of the passthru() commands fails (mpdude) - * bug #35261 [Routing] Fix using a custom matcher & generator dumper class (fancyweb) - * bug #34643 [Dotenv] Fixed infinite loop with missing quote followed by quoted value (naitsirch) - * bug #35239 [Security\Http] Prevent canceled remember-me cookie from being accepted (chalasr) - * bug #35267 [Debug] fix ClassNotFoundFatalErrorHandler (nicolas-grekas) - * bug #35193 [TwigBridge] button_widget now has its title attr translated even if its label = null or false (stephen-lewis) - * bug #35219 [PhpUnitBridge] When using phpenv + phpenv-composer plugin, composer executable is wrapped into a bash script (oleg-andreyev) - * bug #35150 [Messenger] Added check if json_encode succeeded (toooni) - * bug #35170 [FrameworkBundle][TranslationUpdateCommand] Do not output positive feedback on stderr (fancyweb) - * bug #35223 [HttpClient] Don't read from the network faster than the CPU can deal with (nicolas-grekas) - * bug #35214 [DI] DecoratorServicePass should keep container.service_locator on the decorated definition (malarzm) - * bug #35210 [HttpClient] NativeHttpClient should not send >1.1 protocol version (nicolas-grekas) - * bug #33672 [Mailer] Remove line breaks in email attachment content (Stuart Fyfe) - * bug #35101 [Routing] Fix i18n routing when the url contains the locale (fancyweb) - * bug #35124 [TwigBridge][Form] Added missing help messages in form themes (cmen) - * bug #35168 [HttpClient] fix capturing SSL certificates with NativeHttpClient (nicolas-grekas) - * bug #35134 [PropertyInfo] Fix BC issue in phpDoc Reflection library (jaapio) - * bug #35173 [Mailer][MailchimpBridge] Fix missing attachments when sending via Mandrill API (vilius-g) - * bug #35172 [Mailer][MailchimpBridge] Fix incorrect sender address when sender has name (vilius-g) - * bug #35125 [Translator] fix performance issue in MessageCatalogue and catalogue operations (ArtemBrovko) - * bug #35120 [HttpClient] fix scheduling pending NativeResponse (nicolas-grekas) - * bug #35117 [Cache] do not overwrite variable value (xabbuh) - * bug #35113 [VarDumper] Fix "Undefined index: argv" when using CliContextProvider (xepozz) - * bug #35103 [Translation] Use `locale_parse` for computing fallback locales (alanpoulain) - * bug #35094 [Console] Fix filtering out identical alternatives when there is a command loader (fancyweb) - * bug #35039 [DI] skip looking for config class when the extension class is anonymous (nicolas-grekas) - * bug #35049 [ProxyManager] fix generating proxies for root-namespaced classes (nicolas-grekas) - * bug #35022 [Dotenv] FIX missing getenv (mccullagh) - * bug #35025 [HttpClient][Psr18Client] Remove Psr18ExceptionTrait (fancyweb) - * bug #35014 [HttpClient] make pushed responses retry-able (nicolas-grekas) - * bug #35010 [VarDumper] ignore failing __debugInfo() (nicolas-grekas) - * bug #34998 [DI] fix auto-binding service providers to their service subscribers (nicolas-grekas) - * bug #33670 [DI] Service locators can't be decorated (malarzm) - * bug #35000 [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt (fancyweb) - * bug #34996 Fix displaying anonymous classes on PHP 7.4 (nicolas-grekas) - * bug #29839 [Validator] fix comparisons with null values at property paths (xabbuh) - * bug #34900 [DoctrineBridge] Fixed submitting invalid ids when using queries with limit (HeahDude) - * bug #34791 [Serializer] Skip uninitialized (PHP 7.4) properties in PropertyNormalizer and ObjectNormalizer (vudaltsov) - * bug #34956 [Messenger][AMQP] Use delivery_mode=2 by default (lyrixx) - * bug #34915 [FrameworkBundle] Fix invalid Windows path normalization in TemplateNameParser (mvorisek) - * bug #34981 stop using deprecated Doctrine persistence classes (xabbuh) - * bug #34904 [Validator][ConstraintValidator] Safe fail on invalid timezones (fancyweb) - * bug #34955 Require doctrine/persistence ^1.3 (nicolas-grekas) - * bug #34923 [DI] Fix support for immutable setters in CallTrait (Lctrs) - * bug #34918 [Translation] fix memoryleak in PhpFileLoader (nicolas-grekas) - * bug #34920 [Routing] fix memoryleak when loading compiled routes (nicolas-grekas) - * bug #34787 [Cache] Propagate expiry when syncing items in ChainAdapter (trvrnrth) - * bug #34896 [Cache] fix memory leak when using PhpFilesAdapter (nicolas-grekas) - * bug #34438 [HttpFoundation] Use `Cache-Control: must-revalidate` only if explicit lifetime has been given (mpdude) - * bug #34449 [Yaml] Implement multiline string as scalar block for tagged values (natepage) - * bug #34601 [MonologBridge] Fix debug processor datetime type (mRoca) - * bug #34842 [ExpressionLanguage] Process division by zero (tigr1991) - * bug #34902 [PropertyAccess] forward caught exception (xabbuh) - * bug #34888 [TwigBundle] add tags before processing them (xabbuh) - * bug #34762 [Config] never try loading failed classes twice with ClassExistenceResource (nicolas-grekas) - * bug #34839 [Cache] fix memory leak when using PhpArrayAdapter (nicolas-grekas) - * bug #34812 [Yaml] fix parsing negative octal numbers (xabbuh) - * bug #34854 [Messenger] gracefully handle missing event dispatchers (xabbuh) - * bug #34788 [SecurityBundle] Properly escape regex in AddSessionDomainConstraintPass (fancyweb) - * bug #34755 [FrameworkBundle] resolve service locators in `debug:*` commands (nicolas-grekas) - * bug #34832 [Validator] Allow underscore character "_" in URL username and password (romainneutron) - * bug #34776 [DI] fix resolving bindings for named TypedReference (nicolas-grekas) - * bug #34738 [SecurityBundle] Passwords are not encoded when algorithm set to "true" (nieuwenhuisen) - * bug #34779 [Security] do not validate passwords when the hash is null (xabbuh) - * bug #34757 [DI] Fix making the container path-independent when the app is in /app (nicolas-grekas) - -* 4.3.9 (2019-12-01) - - * bug #34649 more robust initialization from request (dbu) - * bug #34671 [Security] Fix clearing remember-me cookie after deauthentication (chalasr) - * bug #34711 Fix the translation commands when a template contains a syntax error (fabpot) - * bug #34560 [Config][ReflectionClassResource] Handle parameters with undefined constant as their default values (fancyweb) - * bug #34695 [Config] don't break on virtual stack frames in ClassExistenceResource (nicolas-grekas) - * bug #34716 [DependencyInjection] fix dumping number-like string parameters (xabbuh) - * bug #34558 [Console] Fix autocomplete multibyte input support (fancyweb) - * bug #34130 [Console] Fix commands description with numeric namespaces (fancyweb) - * bug #34677 [EventDispatcher] Better error reporting when arguments to dispatch() are swapped (rimas-kudelis) - * bug #33573 [TwigBridge] Add row_attr to all form themes (fancyweb) - * bug #34019 [Serializer] CsvEncoder::NO_HEADERS_KEY ignored when used in constructor (Dario Savella) - * bug #34083 [Form] Keep preferred_choices order for choice groups (vilius-g) - * bug #34091 [Debug] work around failing chdir() on Darwin (mary2501) - * bug #34305 [PhpUnitBridge] Read configuration CLI directive (ro0NL) - * bug #34490 [Serializer] Fix MetadataAwareNameConverter usage with string group (antograssiot) - * bug #34632 [Console] Fix trying to access array offset on value of type int (Tavafi) - * bug #34669 [HttpClient] turn exception into log when the request has no content-type (nicolas-grekas) - * bug #34636 [VarDumper] notice on potential undefined index (sylvainmetayer) - * bug #34668 [Cache] Make sure we get the correct number of values from redis::mget() (thePanz) - * bug #34569 [Workflow] Apply the same logic of precedence between the apply() and the buildTransitionBlockerList() method (lyrixx) - * bug #34533 [Monolog Bridge] Fixed accessing static property as non static. (Sander-Toonen) - * bug #34546 [Serializer] Add DateTimeZoneNormalizer into Dependency Injection (jewome62) - * bug #34547 [Messenger] Error when specified default bus is not among the configured (vudaltsov) - * bug #34551 [Security] SwitchUser is broken when the User Provider always returns a valid user (tucksaun) - * bug #34385 Avoid empty "If-Modified-Since" header in validation request (mpdude) - * bug #34458 [Validator] ConstraintValidatorTestCase: add missing return value to mocked validate method calls (ogizanagi) - * bug #34451 [DependencyInjection] Fix dumping multiple deprecated aliases (shyim) - * bug #34448 [Form] allow button names to start with uppercase letter (xabbuh) - * bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas) - * bug #34366 [HttpFoundation] Allow redirecting to URLs that contain a semicolon (JayBizzle) - * bug #34397 [FrameworkBundle] Remove project dir from Translator cache vary scanned directories (fancyweb) - * bug #34408 [Cache] catch exceptions when using PDO directly (xabbuh) - * bug #34410 [HttpFoundation] Fix MySQL column type definition. (jbroutier) - * bug #34398 [Config] fix id-generation for GlobResource (nicolas-grekas) - * bug #34396 [Finder] Allow ssh2 stream wrapper for sftp (damienalexandre) - * bug #34383 [DI] Use reproducible entropy to generate env placeholders (nicolas-grekas) - * bug #34381 [WebProfilerBundle] Require symfony/twig-bundle (fancyweb) - -* 4.3.8 (2019-11-13) - - * bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas) - * security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas) - * security #cve-2019-18888 [Mime] fix guessing mime-types of files with leading dash (nicolas-grekas) - * security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas) - * security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas) - * security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas) - * security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof) - -* 4.3.7 (2019-11-11) - - * bug #34294 [Workflow] Fix error when we use ValueObject for the marking property (FabienSalles) - * bug #34297 [DI] fix locators with numeric keys (nicolas-grekas) - * bug #34282 [DI] Dont cache classes with missing parents (nicolas-grekas) - * bug #34287 [HttpClient] Fix a crash when calling CurlHttpClient::__destruct() (dunglas) - * bug #34129 [FrameworkBundle][Translation] Invalidate cached catalogues when the scanned directories change (fancyweb) - * bug #34246 [Serializer] Use context to compute MetadataAwareNameConverter cache (antograssiot) - * bug #34251 [HttpClient] expose only gzip when doing transparent compression (nicolas-grekas) - * bug #34244 [Inflector] add support for 'species' (jeffreymoelands) - * bug #34085 [Console] Detect dimensions using mode CON if vt100 is supported (rtek) - * bug #34199 [HttpClient] Retry safe requests using HTTP/1.1 when HTTP/2 fails (nicolas-grekas) - * bug #34192 [Routing] Fix URL generator instantiation (X-Coder264, HypeMC) - * bug #34134 [Messenger] fix retry of messages losing the routing key and properties (Tobion) - * bug #34181 [Stopwatch] Fixed bug in getDuration when counting multiple ongoing periods (TimoBakx) - * bug #34165 [PropertyInfo] Fixed type extraction for nullable collections of non-nullable elements (happyproff) - * bug #34179 [Stopwatch] Fixed a bug in StopwatchEvent::getStartTime (TimoBakx) - * bug #34203 [FrameworkBundle] [HttpKernel] fixed correct EOL and EOM month (erics86) - * bug #34035 [Serializer] Fix property name usage for denormalization (antograssiot) - -* 4.3.6 (2019-11-01) - - * bug #34198 [HttpClient] Fix perf issue when doing thousands of requests with curl (nicolas-grekas) - * bug #33998 [Config] Disable default alphabet sorting in glob function due of unstable sort (hurricane-voronin) - * bug #34144 [Serializer] Improve messages for unexpected resources values (fancyweb) - * bug #34186 [HttpClient] always return the empty string when the response cannot have a body (nicolas-grekas) - * bug #34167 [HttpFoundation] Allow to not pass a parameter to Request::isMethodSafe() (dunglas) - * bug #33828 [DoctrineBridge] Auto-validation must work if no regex are passed (dunglas) - * bug #34080 [SecurityBundle] correct types for default arguments for firewall configs (shieldo) - * bug #34152 [Workflow] Made the configuration more robust for the 'property' key (lyrixx) - * bug #34154 [HttpClient] fix handling of 3xx with no Location header - ignore Content-Length when no body is expected (nicolas-grekas) - * bug #34140 [Security/Core] make NativePasswordEncoder use sodium to validate passwords when possible (nicolas-grekas) - * bug #33999 [Form] Make sure to collect child forms created on *_SET_DATA events (yceruto) - * bug #34090 [WebProfilerBundle] Improve display in Email panel for dark theme (antograssiot) - * bug #34116 [HttpClient] ignore the body of responses to HEAD requests (nicolas-grekas) - * bug #32456 [Messenger] use database platform to convert correctly the DateTime (roukmoute) - * bug #34107 [Messenger] prevent infinite redelivery loops and blocked queues (Tobion) - * bug #32341 [Messenger] Show exceptions after multiple retries (TimoBakx) - * bug #34082 Revert "[Messenger] Fix exception message of failed message is dropped (Tobion) - * bug #34021 [TwigBridge] do not render errors for checkboxes twice (xabbuh) - * bug #34017 [Messenger] Fix ignored options in redis transport (chalasr) - * bug #34041 [HttpKernel] fix wrong removal of the just generated container dir (nicolas-grekas) - * bug #34024 [Routing] fix route loading with wildcard, but dir or file is empty (gseidel) - * bug #34023 [Dotenv] allow LF in single-quoted strings (nicolas-grekas) - * bug #33818 [Yaml] Throw exception for tagged invalid inline elements (gharlan) - * bug #33994 [Mailer] Fix Mandrill Transport API payload for named addresses (Michaël Perrin) - * bug #33985 [HttpClient] workaround curl_multi_select() issue (nicolas-grekas) - * bug #33948 [PropertyInfo] Respect property name case when guessing from public method name (antograssiot) - * bug #33962 [Cache] fixed TagAwareAdapter returning invalid cache (v-m-i) - * bug #33958 [DI] Add extra type check to php dumper (gquemener) - * bug #33965 [HttpFoundation] Add plus character `+` to legal mime subtype (ilzrv) - * bug #32943 [Dotenv] search variable values in ENV first then env file (soufianZantar) - * bug #33943 [VarDumper] fix resetting the "bold" state in CliDumper (nicolas-grekas) - * bug #33936 [HttpClient] Missing argument in method_exists (detinkin) - * bug #33937 [Cache] ignore unserialization failures in AbstractTagAwareAdapter::doDelete() (nicolas-grekas) - * bug #33935 [HttpClient] send `Accept: */*` by default, fix removing it when needed (nicolas-grekas) - * bug #33922 [Cache] remove implicit dependency on symfony/filesystem (nicolas-grekas) - * bug #33927 Allow to set SameSite config to 'none' (ihmels) - * bug #33930 [Cache] clean tags folder on invalidation (nicolas-grekas) - * bug #33919 [VarDumper] fix array key error for class SymfonyCaster (zcodes) - * bug #33885 [Form][DateTimeImmutableToDateTimeTransformer] Preserve microseconds and use \DateTime::createFromImmutable() when available (fancyweb) - * bug #33900 [HttpKernel] Fix to populate $dotenvVars in data collector when not using putenv() (mynameisbogdan) - -* 4.3.5 (2019-10-07) - - * bug #33742 [Crawler] document $default as string|null (nicolas-grekas) - * bug #32308 [Messenger] DoctrineTransport: ensure auto setup is only done once (bendavies) - * bug #33871 [HttpClient] bugfix exploding values of headers (michaljusiega) - * bug #33834 [Validator] Fix ValidValidator group cascading usage (fancyweb) - * bug #33863 [Routing] gracefully handle docref_root ini setting (nicolas-grekas) - * bug #33846 [Cache] give 100ms before starting the expiration countdown (nicolas-grekas) - * bug #33853 [HttpClient] fix "no_proxy" option ignored in NativeHttpClient (Harry-Dunne) - * bug #33841 [VarDumper] fix dumping uninitialized SplFileInfo (nicolas-grekas) - * bug #33842 [Cache] fix logger usage in CacheTrait::doGet() (nicolas-grekas) - * bug #33835 [Workflow] Fixed BC break on WorkflowInterface (lyrixx) - * bug #33799 [Security]: Don't let falsy usernames slip through impersonation (j4nr6n) - * bug #33814 [HttpFoundation] Check if data passed to SessionBagProxy::initialize is an array (mynameisbogdan) - * bug #33744 [DI] Add CSV env var processor tests / support PHP 7.4 (ro0NL) - * bug #33805 [FrameworkBundle] Fix wrong returned status code in ConfigDebugCommand (jschaedl) - * bug #33781 [AnnotationCacheWarmer] add RedirectController to annotation cache (jenschude) - * bug #33777 Fix the :only-of-type pseudo class selector (jakzal) - * bug #32051 [Serializer] Add CsvEncoder tests for PHP 7.4 (ro0NL) - * feature #33776 Copy phpunit.xsd to a predictable path (julienfalque) - * bug #33759 [Security/Http] fix parsing X509 emailAddress (nicolas-grekas) - * bug #33733 [Serializer] fix denormalization of string-arrays with only one element (mkrauser) - * bug #33754 [Cache] fix known tag versions ttl check (SwenVanZanten) - * bug #33646 [HttpFoundation] allow additinal characters in not raw cookies (marie) - * bug #33748 [Console] Do not include hidden commands in suggested alternatives (m-vo) - * bug #33625 [DependencyInjection] Fix wrong exception when service is synthetic (k0d3r1s) - * bug #32979 [Messenger] return empty envelopes when RetryableException occurs (surikman) - * bug #32522 [Validator] Accept underscores in the URL validator, as the URL will load (battye) - * bug #32437 Fix toolbar load when GET params are present in "_wdt" route (Molkobain) - * bug #32925 [Translation] Collect original locale in case of fallback translation (digilist) - * bug #33691 [HttpClient] fix race condition when reading response with informational status (nicolas-grekas) - * bug #33727 [HttpClient] workaround bad Content-Length sent by old libcurl (nicolas-grekas) - * bug #31198 [FrameworkBundle] Fix framework bundle lock configuration not working as expected (HypeMC) - * bug #33719 [Cache] dont override native Memcached options (nicolas-grekas) - * bug #33703 [Cache] fail gracefully when locking is not supported (nicolas-grekas) - * bug #33713 Fix exceptions (PDOException) error code type (fruty) - * bug #32335 [Form] Names for buttons should start with lowercase (mcfedr) - * bug #33706 [Mailer][Messenger] ensure legacy event dispatcher compatibility (xabbuh) - * bug #33688 Add missing row_attr option to FormType (mcsky) - * bug #33693 [Security] use LegacyEventDispatcherProxy (dmaicher) - * bug #33675 [PhpUnit] Fix usleep mock return value (fabpot) - * bug #33652 [Cache] skip igbinary on PHP 7.4.0 (nicolas-grekas) - * bug #33643 [HttpClient] fix throwing HTTP exceptions when the 1st chunk is emitted (nicolas-grekas) - * bug #33618 fix tests depending on other components' tests (xabbuh) - * bug #33626 [PropertyInfo] ensure compatibility with type resolver 0.5 (xabbuh) - * bug #33620 [Twig] Fix Twig config extra keys (fabpot) - * bug #33600 [Messenger] Fix exception message of failed message is dropped on retry (tienvx) - * bug #33601 [HttpClient] Add default value for Accept header (numerogeek) - * bug #33340 [Finder] Adjust regex to correctly match comments in gitignore contents (Jeroeny) - * bug #33588 [PropertyInfo] ensure compatibility with type resolver 0.5 (xabbuh) - * bug #33575 [WebProfilerBundle] Fix time panel legend buttons (fancyweb) - * bug #33571 [Inflector] add support 'see' to 'ee' for singularize 'fees' to 'fee' (maxhelias) - * bug #32763 [Console] Get dimensions from stty on windows if possible (rtek) - * bug #33570 Fixed cache pools affecting each other due to an overwritten seed variable (roed) - * bug #33517 [Yaml] properly catch legacy tag syntax usages (xabbuh) - * bug #33546 [DependencyInjection] Accept existing interfaces as valid named args (fancyweb) - * bug #33547 [HttpClient] Re-enable Server Push support (dunglas) - * bug #33521 Fixed incompatibility between ServiceSubscriberTrait and classes with protected $container property (a-menshchikov) - * bug #33518 [Yaml] don't dump a scalar tag value on its own line (xabbuh) - * bug #33505 [HttpClient] fallbackto CURLMOPT_MAXCONNECTS when CURLMOPT_MAX_HOST_CONNECTIONS is not available (nicolas-grekas) - * bug #32818 [HttpKernel] Fix getFileLinkFormat() to avoid returning the wrong URL in Profiler (Arman-Hosseini) - * bug #33487 [HttpKernel] Fix Apache mod_expires Session Cache-Control issue (pbowyer) - * bug #33469 [FrameworkBundle] Fixed suggested package for missing server:dump command (lyrixx) - * bug #31964 [Router] routing cache crash when using generator_class (dFayet) - * bug #33481 [Messenger] fix empty amqp body returned as false (Tobion) - * bug #33387 [Mailer] maintain sender/recipient name in SMTP envelopes (xabbuh) - * bug #33449 Fix gmail relay (Beno!t POLASZEK) - * bug #33391 [HttpClient] fix support for 103 Early Hints and other informational status codes (nicolas-grekas) - * bug #33444 [HttpClient] improve handling of HTTP/2 PUSH, disable it by default (nicolas-grekas) - * bug #33435 [Validator] Only handle numeric values in DivisibleBy (fancyweb) - * bug #33437 Fix #33427 (sylfabre) - * bug #33439 [Validator] Sync string to date behavior and throw a better exception (fancyweb) - * bug #33436 [DI] fix support for "!tagged_locator foo" (nicolas-grekas) - * bug #32903 [PHPUnit Bridge] Avoid registering listener twice (alexpott) - * bug #33432 [Mailer] Fix Mailgun support when a response is not JSON as expected (fabpot) - * bug #33402 [Finder] Prevent unintentional file locks in Windows (jspringe) - * bug #33376 [Mailer] Remove the default dispatcher in AbstractTransport (fabpot) - * bug #33357 [FrameworkBundle] Fix about command not showing .env vars (brentybh) - * bug #33396 Fix #33395 PHP 5.3 compatibility (kylekatarnls) - * bug #33363 [Routing] fix static route reordering when a previous dynamic route conflicts (nicolas-grekas) - * bug #33385 [Console] allow Command::getName() to return null (nicolas-grekas) - * bug #33353 Return null as Expire header if it was set to null (danrot) - * bug #33382 [ProxyManager] remove ProxiedMethodReturnExpression polyfill (nicolas-grekas) - * bug #33377 [Yaml] fix dumping not inlined scalar tag values (xabbuh) - -* 4.3.4 (2019-08-26) - - * bug #33335 [DependencyInjection] Fixed the `getServiceIds` implementation to always return aliases (pdommelen) - * bug #33298 [Messenger] Stop worker when it should stop (tienvx) - * bug #33292 [VarExporter] fix support for PHP 7.4 (nicolas-grekas) - * bug #33282 [HttpKernel] Do not extend the new SF 4.3 ControllerEvent so we can make it final (Tobion) - * bug #33278 [FrameworkBundle] Fix BrowserKit assertions to make them compatible with Panther (dunglas) - * bug #33216 [Mime] Trim and remove line breaks from NamedAddress name arg (maldoinc) - * bug #33124 [Config] Add handling for ignored keys in ArrayNode::mergeValues. (Alexandre Parent) - * bug #33244 [Router] Fix TraceableUrlMatcher behaviour with trailing slash (Xavier Leune) - * bug #33232 Fix handling for session parameters (vkhramtsov) - * bug #32497 [Messenger] DispatchAfterCurrentBusMiddleware does not cancel messages from delayed handlers (Nyholm, BastienClement) - * bug #33127 [Messenger] make delay exchange and queues durable like the normal ones by default (Tobion) - * bug #33210 [Mailer] Don't duplicate addresses in Sendgrid Transport (pierredup) - * bug #33172 [Console] fixed a PHP notice when there is no function in the stack trace of an Exception (fabpot) - * bug #33157 Fix getMaxFilesize() returning zero (ausi) - * bug #33139 [Intl] Cleanup unused language aliases entry (ro0NL) - * bug #33126 [SecurityBundle] display the correct class name on the deprecated notice (maxhelias) - * bug #33093 [EventDispatcher] wrong Request class (maxhelias) - * bug #33092 [DependencyInjection] Improve an exception message (fabpot) - * bug #32541 [HttpKernel] trim the leading backslash in the controller init (Simperfit, fabpot) - * bug #32455 [HttpFoundation] Clear invalid session cookie (Toflar) - * bug #33066 [Serializer] Fix negative DateInterval (jderusse) - * bug #33045 Make HttpClientTestCase compatible with PHPUnit8 (jderusse) - * bug #33033 [Lock] consistently throw NotSupportException (xabbuh) - * bug #33022 [HttpClient] Remove CURLOPT_CONNECTTIMEOUT_MS curl opt (lyrixx) - * bug #32516 [FrameworkBundle][Config] Ignore exceptions thrown during reflection classes autoload (fancyweb) - * bug #33010 [TwigBridge] pass translation parameters to the trans filter (xabbuh) - * bug #32981 Fix tests/code for php 7.4 (jderusse) - * bug #32986 [Mime] fixed wrong mimetype (rjwebdev) - * bug #32992 [ProxyManagerBridge] Polyfill for unmaintained version (jderusse) - * bug #32989 [HttpClient] Declare `$active` first to prevent weird issue (Kocal) - * bug #32999 Added correct plural for box -> boxes (cinamo) - * bug #32933 [PhpUnitBridge] fixed PHPUnit 8.3 compatibility: method handleError was renamed to __invoke (karser) - * bug #32947 [Intl] Support DateTimeInterface in IntlDateFormatter::format (pierredup) - * bug #32919 [Intl] Order alpha2 to alpha3 mapping + phpdoc fixes (ro0NL) - * bug #32792 [Messenger] Fix incompatibility with FrameworkBundle <4.3.1 (chalasr) - * bug #32836 [Messenger] Removed named parameters and replaced with `?` placeholders for sqlsrv compatibility (David Legatt) - * bug #32838 [FrameworkBundle] Detect indirect env vars in routing (ro0NL) - * bug #32918 [Intl] Order alpha2 to alpha3 mapping (ro0NL) - * bug #32902 [PhpUnitBridge] Allow sutFqcnResolver to return array (VincentLanglet) - * bug #32814 Create mailBody with only attachments part present (srsbiz) - * bug #32682 [HttpFoundation] Revert getClientIp @return docblock (ossinkine) - * bug #32910 [Yaml] PHP-8: Uncaught TypeError: abs() expects parameter 1 to be int or float, string given (Aleksandr Dankovtsev) - * bug #32870 #32853 Check if $this->parameters is array. (ABGEO07) - * bug #32899 [Mailer] fix wrong error message when connection closes unexpectedly (fabpot) - * bug #32895 [Mailer] Fix error not being thrown properly (fabpot) - * bug #32868 [PhpUnitBridge] Allow symfony/phpunit-bridge > 4.2 to be installed with phpunit 4.8 (jderusse) - * bug #32823 [HttpClient] Preserve the case of headers when sending them (nicolas-grekas) - * bug #32767 [Yaml] fix comment in multi line value (soufianZantar) - * bug #32790 [HttpFoundation] Fix `getMaxFilesize` (bennyborn) - * bug #32796 [Cache] fix warning on PHP 7.4 (jpauli) - * bug #32806 [Console] fix warning on PHP 7.4 (rez1dent3) - * bug #32809 Don't add object-value of static properties in the signature of container metadata-cache (arjenm) - * bug #32708 Recompile container when translations directory changes (pierredup) - * bug #32722 [DependencyInjection] Fix bindings and tagged_locator (deguif) - * bug #32802 Make sure trace_level is always defined (dbu) - * bug #30096 [DI] Fix dumping Doctrine-like service graphs (bis) (weaverryan, nicolas-grekas) - * bug #32799 [HttpKernel] do not stopwatch sections when profiler is disabled (Tobion) - * bug #32631 [Messenger] expire delay queue and fix auto_setup logic (Tobion) - * bug #32641 [Messenger] Retrieve table default options from the SchemaManager (vincenttouzet) - -* 4.3.3 (2019-07-28) - - * bug #32726 [Messenger] Fix redis last error not cleared between calls (chalasr) - * bug #32760 [HttpKernel] clarify error handler restoring process (xabbuh) - * bug #32730 [Inflector] Fix pluralizing words ending with "son" (norkunas) - * bug #32715 [DI] fix perf issue with lazy autowire error messages (nicolas-grekas) - * bug #32503 Fix multiSelect ChoiceQuestion when answers have spaces (IceMaD) - * bug #32688 [Yaml] fix inline handling when dumping tagged values (xabbuh) - * bug #32710 [Security/Core] align defaults for sodium with PHP 7.4 (nicolas-grekas) - * bug #32644 [WebProfileBundle] Avoid getting right to left style (Arman-Hosseini) - * bug #32689 [HttpClient] rewind stream when using Psr18Client (nicolas-grekas) - * bug #32700 [Messenger] Flatten collection of stamps collected by the traceable middleware (ogizanagi) - * bug #32699 [HttpClient] fix canceling responses in a streaming loop (nicolas-grekas) - * bug #32679 [Intl] relax some date parser patterns (xabbuh) - * bug #31303 [VarDumper] Use \ReflectionReference for determining if a key is a reference (php >= 7.4) (dorumd, nicolas-grekas) - * bug #32485 [Validator] Added support for validation of giga values (kernig) - * bug #32567 [Messenger] pass transport name to factory (Tobion) - * bug #32568 [Messenger] Fix UnrecoverableExceptionInterface handling (LanaiGrunt) - * bug #32604 Properly handle optional tag attributes for !tagged_iterator (apfelbox) - * bug #32571 [HttpClient] fix debug output added to stderr at shutdown (nicolas-grekas) - * bug #32443 [PHPUnitBridge] Mute deprecations triggered from phpunit (greg0ire) - * bug #32572 Bump minimum version of symfony/phpunit-bridge (fancyweb) - * bug #32438 [Serializer] XmlEncoder: don't cast padded strings (ogizanagi) - * bug #32579 [Config] Do not use absolute path when computing the vendor freshness (lyrixx) - * bug #32563 Container*::getServiceIds() should return strings (mathroc) - * bug #32553 [Mailer] Allow register mailer configuration in xml format (Koc) - * bug #32442 Adding missing event_dispatcher wiring for messenger.middleware.send_message (weaverryan) - * bug #32466 [Config] Fix for signatures of typed properties (tvandervorm) - * bug #32501 [FrameworkBundle] Fix descriptor of routes described as callable array (ribeiropaulor) - * bug #32500 [Debug][DebugClassLoader] Include found files instead of requiring them (fancyweb) - * bug #32464 [WebProfilerBundle] Fix Twig 1.x compatibility (yceruto) - * bug #31620 [FrameworkBundle] Inform the user when save_path will be ignored (gnat42) - * bug #32096 Don't assume port 0 for X-Forwarded-Port (alexbowers, xabbuh) - * bug #31820 [SecurityBundle] Fix profiler dump for non-invokable security listeners (chalasr) - * bug #32392 [Messenger] Doctrine Transport: Support setting auto_setup from DSN (bendavies) - * bug #31267 [Translator] Load plurals from mo files properly (Stadly) - * bug #31266 [Translator] Load plurals from po files properly (Stadly) - * bug #32383 [Serializer] AbstractObjectNormalizer ignores the property types of discriminated classes (sandergo90) - * bug #32413 [Messenger] fix publishing headers set on AmqpStamp (Tobion) - * bug #32421 [EventDispatcher] Add tag kernel.rest on 'debug.event_dispatcher' service (lyrixx) - * bug #32398 [Messenger] Removes deprecated call to ReflectionType::__toString() on MessengerPass (brunowowk) - * bug #32379 [SecurityBundle] conditionally register services (xabbuh) - * bug #32380 [Messenger] fix broken key normalization (Tobion) - * bug #32363 [FrameworkBundle] reset cache pools between requests (nicolas-grekas) - * bug #32365 [DI] fix processing of regular parameter bags by MergeExtensionConfigurationPass (nicolas-grekas) - * bug #32187 [PHPUnit] Fixed composer error on Windows (misterx) - * bug #32299 [Lock] Stores must implement `putOffExpiration` (jderusse) - * bug #32302 [Mime] Remove @internal annotations for the serialize methods (francoispluchino) - * bug #32334 [Messenger] Fix authentication for redis transport (alexander-schranz) - * bug #32309 Fixing validation for messenger transports retry_strategy service key (weaverryan) - * bug #32331 [Workflow] only decorate when an event dispatcher was passed (xabbuh) - * bug #32236 [Cache] work aroung PHP memory leak (nicolas-grekas) - * bug #32206 Catch JsonException and rethrow in JsonEncode (phil-davis) - * bug #32211 [Mailer] Fix error message when connecting to a stream raises an error before connect() (fabpot) - * bug #32210 [Mailer] Fix timeout type hint (fabpot) - * bug #32199 [EventDispatcher] improve error messages in the event dispatcher (xabbuh) - * bug #32200 [Security/Core] work around sodium_compat issue (nicolas-grekas) - -* 4.3.2 (2019-06-26) - - * bug #31954 [PhpunitBridge] Read environment variable from superglobals (greg0ire) - * bug #32131 [Mailgun Mailer] fixed issue when using html body (alOneh) - * bug #31730 [PhpUnitBridge] More accurate grouping (greg0ire) - * bug #31966 [Messenger] Doctrine Connection find and findAll now correctly decode headers (TimoBakx) - * bug #31972 Add missing rendering of form help block. (alexsegura) - * bug #32141 [HttpClient] fix dealing with 1xx informational responses (nicolas-grekas) - * bug #32138 [Filesystem] fix mirroring directory into parent directory (xabbuh) - * bug #32137 [HttpFoundation] fix accessing session bags (xabbuh) - * bug #32147 [HttpClient] fix timing measurements with NativeHttpClient (nicolas-grekas) - * bug #32165 revert #30525 due to performance penalty (bendavies) - * bug #32164 [EventDispatcher] collect called listeners information only once (xabbuh) - * bug #32173 [FrameworkBundle] Fix calling Client::getProfile() before sending a request (dunglas) - * bug #32163 [DoctrineBridge] Fix type error (norkunas) - * bug #32154 [Messenger] fix retrying handlers using DoctrineTransactionMiddleware (Tobion) - * bug #32169 [Security/Core] require libsodium >= 1.0.14 (nicolas-grekas) - * bug #32170 [Security/Core] Don't use ParagonIE_Sodium_Compat (nicolas-grekas) - * bug #32156 [Workflow] re-add workflow.definition tag to workflow services (nikossvnk) - * bug #32053 [Messenger] No need for retry to require SentStamp (Tobion) - * bug #32083 [HttpClient] fixing passing debug info to progress callback (nicolas-grekas) - * bug #32129 [DebugBundle] fix register ReflectionCaster::unsetClosureFileInfo caster in var cloner service (alekitto) - * bug #32027 [Messenger] Remove DispatchAfterCurrentBusStamp when message is put on internal queue (Nyholm) - * bug #32125 [Form] accept floats for input="string" in NumberType (xabbuh) - * bug #32094 [Validator] Use LogicException for missing Property Access Component in comparison constraints (Lctrs) - * bug #32136 [FrameworkBundle] sync `require-dev` and `conflict` constraints (xabbuh) - * bug #32123 [Form] fix translation domain (xabbuh) - * bug #32115 [SecurityBundle] don't validate IP addresses from env var placeholders (xabbuh) - * bug #32116 [FrameworkBundle] tag the FileType service as a form type (xabbuh) - * bug #32109 [Messenger] fix delay exchange recreation after disconnect (Tobion) - * bug #32090 [Debug] workaround BC break in PHP 7.3 (nicolas-grekas) - * bug #32076 [Lock] Fix PDO prune not called (jderusse) - * bug #32071 Fix expired lock not cleaned (jderusse) - * bug #32052 [Messenger] fix AMQP delay queue to be per exchange (Tobion) - * bug #32065 [HttpClient] throw DecodingExceptionInterface when toArray() fails because of content-type error (nicolas-grekas) - * bug #32057 [HttpFoundation] Fix SA/phpdoc JsonResponse (ro0NL) - * bug #32040 [DI] Show the right class autowired when providing a non-existing class (Simperfit) - * bug #32035 [Messenger] fix delay delivery for non-fanout exchanges (Tobion) - * bug #32025 SimpleCacheAdapter fails to cache any item if a namespace is used (moufmouf) - * bug #32022 [HttpClient] Don't use CurlHttpClient on Windows when curl.cainfo is not set (nicolas-grekas) - * bug #32037 [Form] validate composite constraints in all groups (xabbuh) - * bug #32007 [Serializer] Handle true and false appropriately in CSV encoder (battye) - * bug #32036 [Messenger] improve logs (Tobion) - * bug #31998 Parameterize Mailgun's region (jderusse) - * bug #32000 [Routing] fix absolute url generation when scheme is not known (Tobion) - * bug #32012 Add statement to fileLink to ignore href code when no fileLink. (bmxmale) - * bug #32024 [VarDumper] fix dumping objects that implement __debugInfo() (nicolas-grekas) - * bug #32014 Do not log or call the proxy function when the locale is the same (gmponos) - * bug #32011 [HttpClient] fix closing debug stream prematurely (nicolas-grekas) - * bug #32017 [Contracts] add missing required dependencies (mbessolov) - * bug #31992 Fix sender/recipients in SMTP Envelope (fabpot) - * bug #31999 [PhpunitBridge] Restore php 5.5 compat (greg0ire) - * bug #31991 [EventDispatcher] collect called listeners information only once (xabbuh) - * bug #31988 [TwigBridge] add back possibility to use form themes without translations (xabbuh) - * bug #31982 [HttpClient] fix Psr18Client handling of non-200 response codes (nicolas-grekas) - * bug #31953 [DoctrineBridge] fix handling nested embeddables (xabbuh) - * bug #31962 Fix reporting unsilenced deprecations from insulated tests (nicolas-grekas) - * bug #31936 PropertyInfoLoader should not try to add validation to non-existent property (weaverryan) - * bug #31923 [Serializer] Fix DataUriNormalizer deprecation (MIME type guesser is optional) (ogizanagi) - * bug #31928 [FrameworkBundle] avoid service id conflicts with Swiftmailer (xabbuh) - * bug #31925 [Form] fix usage of legacy TranslatorInterface (nicolas-grekas) - * bug #31908 [Validator] fix deprecation layer of ValidatorBuilder (nicolas-grekas) - -* 4.3.1 (2019-06-06) - - * bug #31894 Fix wrong requirements for ocramius/proxy-manager in root composer.json (henrikvolmer) - * bug #31865 [Form] Fix wrong DateTime on outdated ICU library (aweelex) - * bug #31893 [HttpKernel] fix link to source generation (nicolas-grekas) - * bug #31880 [FrameworkBundle] fix BC-breaking property in WebTestAssertionsTrait (nicolas-grekas) - * bug #31881 [FramworkBundle][HttpKernel] fix KernelBrowser BC layer (nicolas-grekas) - * bug #31879 [Cache] Pass arg to get callback everywhere (fancyweb) - * bug #31874 [Doctrine Bridge] Check field type before adding Length constraint (belinde) - * bug #31872 [Messenger] Add missing runtime check for ext redis version (chalasr) - * bug #31864 [Cache] Fixed undefined variable in ArrayTrait (eXtreme) - * bug #31863 [HttpFoundation] Fixed case-sensitive handling of cache-control header in RedirectResponse constructor (Ivo) - * bug #31850 [HttpClient] add $response->cancel() (nicolas-grekas) - * bug #31871 [HttpClient] revert bad logic around JSON_THROW_ON_ERROR (nicolas-grekas) - * bug #31869 Fix json-encoding when JSON_THROW_ON_ERROR is used (nicolas-grekas) - * bug #31868 [HttpKernel] Fix handling non-catchable fatal errors (nicolas-grekas) - * bug #31834 [HttpClient] Don't throw InvalidArgumentException on bad Location header (nicolas-grekas) - * bug #31846 [Mailer] Set default crypto method (bpolaszek) - * bug #31849 [Console] Add check for Konsole/Yakuake to disable hyperlinks (belinde) - * bug #31854 Rename the Symfony Mailer service implementation to avoid conflict with SwitMailer (tgalopin) - * bug #31856 [VarDumper] fix dumping the cloner itself (nicolas-grekas) - * bug #31861 [HttpClient] work around PHP 7.3 bug related to json_encode() (nicolas-grekas) - * bug #31860 [HttpFoundation] work around PHP 7.3 bug related to json_encode() (nicolas-grekas) - * bug #31852 [Form] add missing symfony/service-contracts dependency (nicolas-grekas) - * bug #31836 [DoctrineBridge] do not process private properties from parent class (xabbuh) - * bug #31790 [Messenger] set amqp content_type based on serialization format (Tobion) - * bug #31832 [HttpClient] fix unregistering the debug buffer when using curl (nicolas-grekas) - * bug #31407 [Security] added support for updated "distinguished name" format in x509 authentication (Robert Kopera) - * bug #31774 [Mailer] Fix the possibility to set a From header from MessageListener (fabpot) - * bug #31811 [DoctrineBridge] don't add embedded properties to wrapping class metadata (xabbuh) - * bug #31786 [Translation] Fixed case sensitivity of lint:xliff command (javiereguiluz) - * bug #31815 [Translator] Collect locale details earlier in the process (pierredup) - * bug #31761 [TwigBridge] suggest Translation Component when TranslationExtension is used (nicolas-grekas) - * bug #31748 [Messenger] Inject RoutableMessageBus instead of bus locator (chalasr) - * bug #31763 [Security\Core] Make SodiumPasswordEncoder validate BCrypt-ed passwords (nicolas-grekas) - * bug #31744 [Validator] Fix TimezoneValidator default option (ro0NL) - * bug #31749 [DoctrineBridge][Validator] do not enable validator auto mapping by default (xabbuh) - * bug #31757 [DomCrawler] Fix type error with null Form::$currentUri (chalasr) - * bug #31721 [PHPUnitBridge] Use a more appropriate group when deprecating mode (greg0ire) - -* 4.3.0 (2019-05-30) - - * bug #31654 [HttpFoundation] Do not set X-Accel-Redirect for paths outside of X-Accel-Mapping (vilius-g) - -* 4.3.0-RC1 (2019-05-28) - - * bug #31650 Create an abstract HTTP transport and extend it in all HTTP transports (bocharsky-bw) - * feature #31641 [HttpClient] make $response->getInfo('debug') return extended logs about the HTTP transaction (nicolas-grekas) - * feature #31571 [Contracts] split in one package per sub-contracts (nicolas-grekas) - * bug #31625 [Messenger] Disable the SchemaAssetsFilter when setup the transport (vincenttouzet) - * bug #31621 [Messenger] Fix missing auto_setup for RedisTransport (chalasr) - * bug #31584 [Workflow] Do not trigger extra guards (lyrixx) - * bug #31632 [Messenger] Use "real" memory usage to honor --memory-limit (chalasr) - * bug #31610 [HttpClient] fix handling exceptions thrown before first mock chunk (nicolas-grekas) - * bug #31615 Allow WrappedListener to describe uncallable listeners (derrabus) - * bug #31599 [Translation] Fixed issue with new vs old TranslatorInterface in TranslationDataCollector (althaus) - * bug #31565 [Mime][HttpFoundation] Added mime type audio/x-hx-aac-adts (ifaridjalilov) - * bug #31591 [FrameworkBundle] fix named autowiring aliases for TagAwareCacheInterface (nicolas-grekas) - * bug #31590 [Cache] improve logged messages (nicolas-grekas) - * bug #31586 [HttpClient] display proper error message on TransportException when curl is used (nicolas-grekas) - * bug #31349 [WebProfilerBundle] Use absolute URL for profiler links (Alumbrados) - * bug #31541 [DI] fix using bindings with locators of service subscribers (nicolas-grekas) - * bug #31568 [Process] Fix infinite waiting for stopped process (mshavliuk) - -* 4.3.0-BETA2 (2019-05-22) - - * bug #31569 [HttpClient] Only use CURLMOPT_MAX_HOST_CONNECTIONS & CURL_VERSION_HTTP2 if defined (GawainLynch) - * bug #31566 [Security] fixed a fatal error when upgrading from 4.2 (fabpot) - * bug #31219 [HttpClient] Allow arrays as query parameters (sleepyboy) - * bug #31482 [Messenger][DoctrineBridge] Throws UnrecoverableMessageHandlingException when passed invalid entity manager name (Koc) - * feature #31471 [Messenger] Add "non sendable" stamps (weaverryan) - * bug #31545 [Messenger] Fix redis Connection::get() should be non blocking by default (chalasr) - * bug #31537 [Workflow] use method marking store (noniagriconomie) - * bug #31551 [ProxyManager] isProxyCandidate() does not take into account interfaces (andrerom) - * bug #31542 [HttpClient] add missing argument check (nicolas-grekas) - * bug #31544 [Messenger] Fix undefined index on read timeout (chalasr) - * bug #31335 [Doctrine] Respect parent class contract in ContainerAwareEventManager (Koc) - * bug #31421 [Routing][AnnotationClassLoader] fix utf-8 encoding in default route name (przemyslaw-bogusz) - * bug #31493 [EventDispatcher] Removed "callable" type hint from WrappedListener constructor (wskorodecki) - * bug #31502 [WebProfilerBundle][Form] The form data collector return serialized data (Simperfit) - * bug #31510 Use the current working dir as default first arg in 'link' binary (lyrixx) - * bug #31524 [HttpFoundation] prevent deprecation when filesize matches error code (xabbuh) - * bug #31535 [Debug] Wrap call to require_once in a try/catch (lyrixx) - * feature #31030 [Serializer] Deprecate calling createChildContext without the format parameter (dbu) - * bug #31459 Fix the interface incompatibility of EventDispatchers (keulinho) - * bug #31463 [DI] default to service id - *not* FQCN - when building tagged locators (nicolas-grekas) - * feature #31294 [Form] Add intl/choice_translation_locale option to TimezoneType (ro0NL) - * feature #31452 [FrameworkBundle] Add cache configuration for PropertyInfo (alanpoulain) - * feature #31486 [Doctrine][PropertyInfo] Detect if the ID is writeable (dunglas) - * bug #31481 [Validator] Autovalidation: skip readonly props (dunglas) - * bug #31480 Update dependencies in the main component (DavidPrevot) - * bug #31477 [PropertyAccess] Add missing property to PropertyAccessor (vudaltsov) - * bug #31479 [Cache] fix saving unrelated keys in recursive callback calls (nicolas-grekas) - * bug #30930 [FrameworkBundle] Fixed issue when a parameter contains a '' (lyrixx) - * bug #31438 [Serializer] Fix denormalization of object with variadic constructor typed argument (ajgarlag) - * bug #31445 [Messenger] Making cache rebuild correctly when message subscribers change (weaverryan) - * bug #31442 [Validator] Fix finding translator parent definition in compiler pass (deguif) - * bug #31475 [HttpFoundation] Allow set 'None' on samesite cookie flag (markitosgv) - * feature #31454 [Messenger] remove send_and_handle which can be achieved with SyncTransport (Tobion) - * bug #31425 [Messenger] On failure retry, make message appear received from original sender (weaverryan) - * bug #31472 [Messenger] Fix routable message bus default bus (weaverryan) - * bug #31465 [Serializer] Fix BC break: DEPTH_KEY_PATTERN must be public (dunglas) - * bug #31458 [TwigBundle] Fix Mailer integration in Twig (fabpot) - * bug #31456 Remove deprecated usage of some Twig features (fabpot) - * bug #31207 [Routing] Fixed unexpected 404 NoConfigurationException (yceruto) - * bug #31261 [Console] Commands with an alias should not be recognized as ambiguous when using register (Simperfit) - * bug #31371 [DI] Removes number of elements information in debug mode (jschaedl) - * bug #31418 [FrameworkBundle] clarify the possible class/interface of the cache (xabbuh) - -* 4.3.0-BETA1 (2019-05-09) - - * feature #31249 [Translator] Set sources when extracting strings from php files (Stadly) - * feature #31365 [Intl] Made countries ISO 3166 compliant + exclude Zzzz script code (ro0NL) - * feature #31060 [Validator] Make API endpoint for NotCompromisedPasswordValidator configurable (xelan) - * feature #31353 [FrameworkBundle] Show injected services for iterator and array arguments (jschaedl) - * feature #31350 [Intl] Rename Regions to Countries (ro0NL) - * feature #31364 [Bridge/PhpUnit] Extract all the code but shebang from bin/simple-phpunit (JustBlackBird) - * feature #30985 [Form] Keep preferred choices order in ChoiceType (vudaltsov) - * feature #31288 [Messenger] RoutableMessageBus route to default bus (dirk39) - * feature #31292 [Validator] Allow intl timezones (ro0NL) - * feature #30970 [Messenger] Adding failure transport support (weaverryan) - * feature #31318 [Intl] Compile localized timezone offset name (ro0NL) - * feature #31248 [Translator] Add sources when dumping qt files (Stadly) - * feature #31280 [WebServerBundle] Change the default pidfile location to cache directory (jschaedl) - * feature #31293 [Form] Remove default option grouping in TimezoneType (ro0NL) - * feature #31262 [Intl] Update timezones to ICU 64.2 + compile zone to country mapping (ro0NL) - * feature #31295 [Intl] Add timezone offset utilities (ro0NL) - * feature #30958 [Messenger] Allows to register handlers on a specific transport (sroze) - * feature #31061 [BridgeDoctrineMessenger] Doctrine ping connection middleware (insidestyles) - * feature #31282 [Messenger] Add WorkerStoppedEvent (chalasr) - * feature #31138 [Security] Dispatch an event when "logout user on change" steps in (Simperfit) - * feature #31242 Update LoggingTranslator to log the change of a locale (gmponos) - * feature #30917 [Messenger] Add a redis stream transport (soyuka, alexander-schranz) - * feature #31195 [Form] Add intltimezone input to TimezoneType (ro0NL) - * feature #31134 [Routing] do not encode comma in query and fragment (Tobion) - * feature #31220 [TwigBridge] bootstrap4 file_widget: allow setting label attributes declared in label_attr (AngelFQC) - * feature #31204 [Messenger] ease testing and allow forking the middleware stack (nicolas-grekas) - * feature #30370 [Cache] Add optimized FileSystem & Redis TagAware Adapters (andrerom) - * feature #28831 [Intl] Add Timezones (ro0NL) - * feature #31170 [Security] deprecate BCryptPasswordEncoder in favor of NativePasswordEncoder (nicolas-grekas) - * feature #31140 [Security] Add NativePasswordEncoder (nicolas-grekas) - * feature #31130 [VarDumper] add caster for WeakReference instances of PHP 7.4 (nicolas-grekas) - * feature #31082 [Form] Show all option normalizers on debug:form command (yceruto) - * feature #30957 [Messenger] Remove base64_encode & use addslashes (weaverryan) - * feature #30717 [Serializer] Use name converter when normalizing constraint violation list (norkunas) - * feature #28846 [Intl] Simplify API (ro0NL) - * feature #31093 [PhpUnitBridge] ClockMock does not mock gmdate() (Simperfit) - * feature #29211 [PhpUnitBridge] Url encoded deprecations helper config (greg0ire) - * feature #31062 [Dotenv] Deprecate useage of "putenv" (Nyholm) - * feature #31021 [Cache] Added command for list all available cache pools (Nyholm) - * feature #31027 [Config] Deprecate TreeBuilder::root (gharlan) - * feature #31019 [Security] Replace Argon2*PasswordEncoder by SodiumPasswordEncoder (chalasr) - * feature #30997 [Console] Add callback support to Console\Question autocompleter (Mikkel Paulson) - * feature #30959 [FrameworkBundle] [TwigBundle] Move the hinclude key away from templating (Simperfit) - * feature #30968 [Security] Add Argon2idPasswordEncoder (chalasr) - * feature #30963 [Serializer] Experimental for ObjectListExtractor (joelwurtz) - * feature #30933 [Routing][ObjectRouteLoader] Allow invokable route loader services (fancyweb) - * feature #30897 [DIC] Add a `require` env var processor (mpdude) - * feature #30964 [HttpKernel] Add a "short" trace header format, make header configurable (mpdude) - * feature #29935 [DI] Fix bad error message for unused bind under _defaults (przemyslaw-bogusz) - * feature #30962 [DoctrineBridge] Deprecated implicit optimization in DoctrineChoiceLoader (HeahDude) - * feature #30862 [Routing] UrlHelper to get absolute URL for a path (vudaltsov) - * feature #30607 [Serializer] Add Support of recursive denormalization on object_to_populate (jewome62) - * feature #30429 [Form] group_by as callback returns array (antonch1989) - * feature #30887 [FrameworkBundle] fix search in debug autowiring (sez-open) - * feature #30935 Use env variable to create anytype of lock store (jderusse) - * feature #30932 [Validator] Add an option to disable NotCompromisedPasswordValidator (lyrixx) - * feature #30909 [Translator] Add comments when dumping po files (deguif) - * feature #30913 [Messenger] Uses an `AmqpStamp` to provide flags and attributes (sroze) - * feature #30900 [Validator] add new `Timezone` validation constraint (phansys) - * feature #30915 [Serializer] Add datetimezone normalizer (jewome62) - * feature #28937 Improve Translator caching (rpkamp) - * feature #30904 [Serializer] provide new ObjectPropertyListExtractorInterface (dmaicher) - * feature #30902 [Workflow] The TransitionEvent is able to modify the context (lyrixx) - * feature #30908 [Workflow] Added workflow_transition_blockers twig function (lyrixx) - * feature #30893 Add "input" option to NumberType (fancyweb, Bernhard Schussek) - * feature #30898 [Validator] Wire NotCompromisedPassword in FrameworkBundle and handle non UTF-8 password (tgalopin) - * feature #30890 [Workflow] Changed initial_places to initial_marking, added property (HeahDude, lyrixx) - * feature #30906 [symfony/HttpKernel] Throws an error when the generated class name is invalid. (drupol) - * feature #30892 [DomCrawler] Improve Crawler HTML5 parser need detection (tgalopin) - * feature #30901 Renamed NotPwned to NotCompromisedPassword (javiereguiluz) - * feature #30020 [Messenger] Ensure message is handled only once per handler (keulinho, sroze) - * feature #30545 #30536 PropertyAccessor->getValue disable exception (dimabory) - * feature #30008 [messenger] Adds a stamp to provide a routing key on message publishing (G15N, sroze) - * feature #29097 [Messenger] Add a "in-memory://" transport (GaryPEGEOT, sroze) - * feature #30537 [HttpClient] logger integration (antonch1989, nicolas-grekas) - * feature #30853 [Twig] Remove TemplatedEmail::template() (fabpot) - * feature #30757 [Messenger] Adding MessageCountAwareInterface to get transport message count (weaverryan) - * feature #28929 [HttpKernel][Framework] Locale aware services (neghmurken) - * feature #29306 [DomCrawler] Optionally use html5-php to parse HTML (tgalopin) - * feature #30255 [DependencyInjection] Invokable Factory Services (zanbaldwin) - * feature #30843 [HttpClient] Add ScopingHttpClient::forBaseUri() + tweak MockHttpClient (nicolas-grekas) - * feature #30844 [Cache] add logs on early-recomputation and locking (nicolas-grekas) - * feature #30520 [RouterDebugCommand] add link to Controllers (nicoweb) - * feature #30212 [DI] Add support for "wither" methods - for greater immutable services (nicolas-grekas) - * feature #30674 [FrameworkBundle] change the way http clients are configured by leveraging ScopingHttpClient (nicolas-grekas) - * feature #29312 [EventDispatcher] Split events across requests (ro0NL) - * feature #30827 [TwigBridge] Add template file link to debug:twig command (yceruto) - * feature #30826 [Form] Add file links for described classes in debug:form command (yceruto) - * feature #30813 New PHPUnit assertions for the WebTestCase (Pierstoval, fabpot) - * feature #27738 [Validator] Add a HaveIBeenPwned password validator (dunglas) - * feature #30690 Changing messenger bus id from 'message_bus' to 'messenger.default_bus' (THERAGE Kévin) - * feature #30810 [Inflector] remove "internal" marker from the component (nicolas-grekas) - * feature #26890 [Inflector] Support pluralization in the inflector (mbabker) - * feature #28637 [Validator] add number constraints (jschaedl) - * feature #30754 [Messenger] New messenger:stop-workers Command (weaverryan) - * feature #30707 [Messenger][DX] Allow stamps to be passed directly to MessageBusInterface::dispatch() (weaverryan) - * feature #29007 [Messenger] Add a Doctrine transport (vincenttouzet) - * feature #30628 Making the serializer configurable by transport (weaverryan) - * feature #30569 [FrameworkBundle][HttpKernel] Provide intuitive error message when a controller fails because it's not registered as a service (moynzzz) - * feature #26484 [Validator] String normalization options for string-based validators (renan-taranto) - * feature #30320 [Form][TwigBridge] Add row_attr to form theme (alexander-schranz) - * feature #30371 [OptionsResolver] Add a new method addNormalizer and normalization hierarchy (yceruto) - * feature #27735 [Validator][DoctrineBridge][FWBundle] Automatic data validation (dunglas) - * feature #30758 [PropertyAccess] Allow Can Accessor in Property Access (ragboyjr) - * feature #30116 [Filesystem] Fix mirroring a directory into itself or in his child with realpath checks (Fleuv, XuruDragon) - * feature #28879 [Debug] Mimic __toString php behavior in FlattenException (Deltachaos) - * feature #29495 [Ldap] Implement pagination (kevans91) - * feature #29448 [Ldap] Entry move support (kevans91) - * feature #30741 Add the Mailer component (fabpot) - * feature #30780 Fix some exception previous type hints (fabpot) - * feature #30729 [HttpKernel] change $previous argument for HttpException to \Throwable (sGy1980de) - * feature #30744 [Finder] Throw a dedicated exception for non-existing directory (xelan) - * feature #30759 [Messenger] Adding the "sync" transport to call handlers synchronously (weaverryan) - * feature #30772 [Contracts][EventDispatcher] move the Event class to symfony/contracts (nicolas-grekas) - * feature #30708 [Messenger] ReceiverInterface::handle() to get() & Worker with prioritized transports (weaverryan) - * feature #27648 [Lock] Added MongoDBStore (Joe Bennett) - * feature #30752 [HttpClient] use "nyholm/psr7" by default in Psr18Client (nicolas-grekas) - * feature #30671 Add optional parameter `prefetching` for AMQP connection (fbouchery) - * feature #25707 [DI] ServiceProviderInterface, implementation for ServiceLocator (kejwmen) - * feature #30606 [Validator] allow brackets in the optional query string (Emmanuel BORGES) - * feature #29476 [Messenger] Add a command to setup transports (vincenttouzet) - * feature #30719 [Mime] Add BodyRendererInterface (fabpot) - * feature #30664 [Finder] Get filename without extension (antonch1989) - * feature #30645 Alias for each assets package (gpenverne) - * feature #30706 [PropertyInfo] Add possibility to extract private and protected properties in reflection extractor (joelwurtz) - * feature #27808 [DI] Deprecate non-string default envs (ro0NL) - * feature #30691 [Contracts][EventDispatcher] add EventDispatcherInterface to symfony/contracts and use it where possible (nicolas-grekas) - * feature #20978 [Form] TransformationFailedException: Support specifying message to display (ogizanagi) - * feature #30676 Avoid dispatching SendMessageToTransportsEvent on redeliver (weaverryan) - * feature #26555 [Validator] Add constraint on unique elements collection(Assert\Unique) (zenmate, nicolas-grekas) - * feature #27684 [FrameworkBundle] Debug container environment variables (ro0NL) - * feature #30666 [Form][Console] Use dumper (ro0NL) - * feature #30559 [HttpClient] Parse common API error formats for better exception messages (dunglas) - * feature #28898 [Console] Add dumper (ro0NL) - * feature #30629 [HttpClient] added CachingHttpClient (fabpot) - * feature #30602 [BrowserKit] Add support for HttpClient (fabpot, THERAGE Kévin) - * feature #30651 Allow user to set the project dir (tdutrion) - * feature #30654 [HttpClient] Add a ScopingHttpClient (XuruDragon) - * feature #30388 [Security] undeprecate the RoleHierarchyInterface (xabbuh) - * feature #30652 Fixing a bug where messenger:consume could send message to wrong bus (weaverryan) - * feature #30650 Dispatching two events when a message is sent & handled (weaverryan) - * feature #30557 [Messenger] Worker events + global retry functionality (weaverryan) - * feature #30468 [Workflow] Added support for many inital places (lyrixx) - * feature #30448 [Finder] Ignore paths from .gitignore #26714 (amaabdou) - * feature #30625 [HttpKernel] add RealHttpKernel: handle requests with HttpClientInterface (fabpot) - * feature #30508 [Routing] Exposed "utf8" option, defaults "locale" and "format" in configuration (Jules Pietri) - * feature #28920 [EventDispatcher] swap arguments of dispatch() to allow registering events by FQCN (nicolas-grekas) - * feature #30605 [Cache] added DSN support for rediss in AbstractAdapter and RedisTrait (alex-vasilchenko-md) - * feature #30604 [HttpClient] add MockHttpClient (nicolas-grekas) - * feature #21035 [FrameworkBundle] Deprecate the Templating component integration (dunglas, fabpot) - * feature #30567 [HttpClient] exceptions carry response (antonch1989) - * feature #28849 [Messenger] Support for handling messages after current bus is finished (Nyholm) - * feature #29538 [Workflow] Add colors to workflow dumps (alexislefebvre) - * feature #28975 [DI] Add an url EnvProcessor (jderusse) - * feature #30419 [FrameworkBundle] Add integration of http-client component (Ioni14, nicoweb) - * feature #30583 [Messenger] Display a nice error when connection fail (lyrixx) - * feature #30450 [Profiler] Render the performance graph with SVG (Tom32i) - * feature #29130 [Serializer] Normalize constraint violation parameters (ogizanagi) - * feature #28330 [MonologBridge] Add monolog processors adding route and command info (trakos) - * feature #30339 [Monolog] Disable DebugLogger in CLI (lyrixx) - * feature #30584 [Intl] Add compile binary (ro0NL) - * feature #30579 Using AMQP auto-setup in all cases, not just in debug (weaverryan) - * feature #30348 [DependencyInjection] Add ability to define an index for service in an injected service locator argument (XuruDragon, nicolas-grekas) - * feature #30469 Create a hyperlink to interfaces/classes that can be autowired (SerkanYildiz) - * feature #30334 [DI] add ReverseContainer: a locator that turns services back to their ids (nicolas-grekas) - * feature #30539 [Messenger] deprecate LoggingMiddleware in favor of providing a logger to SendMessageMiddleware (nicolas-grekas) - * feature #30556 [HttpClient] Allow to pass user/pw as an array (dunglas) - * feature #30547 [HttpClient] Add new bearer option (dunglas) - * feature #29303 [Messenger] add welcome notice when running the command (nicolas-grekas) - * feature #30541 [BrowserKit] Rename Client to Browser (fabpot) - * feature #30504 [DI] replace "nullable" env processor by improving the "default" one (nicolas-grekas) - * feature #30499 [HttpClient] add ResponseInterface::toArray() (nicolas-grekas) - * feature #30472 [Translation] Add XLIFF 1 source to metadata to differentiate from attr (ostrolucky) - * feature #30484 [Mime] added Headers::toArray() (fabpot) - * feature #30482 [Mime] Fix support for date form parts (fabpot) - * feature #30385 [SecurityBundle] Validate the IPs configured in access_control (javiereguiluz) - * feature #30413 [HttpClient][Contracts] introduce component and related contracts (nicolas-grekas) - * feature #30377 [Validator] add MIR card scheme (antonch1989) - * feature #29146 [Workflow] Added a context to `Workflow::apply()` (lyrixx) - * feature #30433 [Form] Allow to disable and customize PercentType symbol (Ken Stanley, OskarStark) - * feature #30408 [HttpKernel] Better exception page when the invokable controller returns nothing (dimabory) - * feature #30325 [HttpKernel] Prevent search engines from indexing dev applications (GaryPEGEOT) - * feature #30390 [FrameworkBundle] Fix UrlGenerator::generate to return an empty string instead of null (Emmanuel BORGES) - * feature #30375 [Messenger] Added transport agnostic exception (nikossvnk, lolmx) - * feature #29254 [FrameworkBundle] Added the condition routing option to the debug router command (soufianZantar) - * feature #30286 Drop more usages of Serializable (nicolas-grekas) - * feature #30379 [FrameworkBundle][Routing] allow boolean container parameters for routes (dmaicher) - * feature #29661 [Filesystem] Support resources and deprecate using arrays in dumpFile() and appendToFile() (thewilkybarkid) - * feature #30358 [Form] be able to specify the input format for times (xabbuh) - * feature #30416 Mime messages (fabpot) - * feature #22048 [Security] deprecate the Role and SwitchUserRole classes (xabbuh) - * feature #30345 [Monolog] Added a way to configure the ConsoleFormatter from the ConsoleHandler (lyrixx) - * feature #30357 [TwigBridge] rename parent_form() to form_parent() (xabbuh) - * feature #30257 [DependencyInjection] Allow to choose an index for tagged collection (deguif, XuruDragon) - * feature #30311 [VarDumper] Implement DsCaster (enumag) - * feature #27570 [PropertyInfo] Added support for extract type from default value (tsantos84) - * feature #28919 [DX][WebProfilerBundle] Add Pretty Print functionality for Request Content (SamFleming) - * feature #28723 [Form] deprecate custom formats with HTML5 widgets (xabbuh) - * feature #29865 [Console] Added suggestions for missing packages (przemyslaw-bogusz) - * feature #30301 [VarDumper] add link to source next to class names (nicolas-grekas) - * feature #30225 publish message with custom queue options : flags | attributes (fedor.f, insidestyles) - * feature #30249 [Routing] deprecate some router options (Tobion) - * feature #30267 [Form] add option to render NumberType as type="number" (xabbuh) - * feature #28969 [Form] deprecate using invalid names for buttons (xabbuh) - * feature #29887 [Form] Add input_format option to DateType and DateTimeType (fancyweb) - * feature #30051 Drop \Serializable implementations (renanbr) - * feature #30236 Add element to ghost in Exception (przemyslaw-bogusz) - * feature #30120 [FrameworkBundle][Translation] Added support for PHP files with trans() in translation commands (yceruto) - * feature #28812 [Form] add a convenience method to get the parent form in Twig templates (xabbuh) - * feature #29121 [FrameworkBundle][Translation] Add support for Translator paths, Twig paths and Translator aware services paths in commands (yceruto) - * feature #28477 [Validator] Add new json Validator (zairigimad) - * feature #30126 [Form] forward valid numeric values to transform() (xabbuh) - * feature #28635 [Form] Add label_translation_parameters, help_translation_parameters and attr_translation_parameters options to base form type (webnet-fr) - * feature #29767 Nullable environment variable processor (bpolaszek) - * feature #30111 [SecurityBundle] Deprecate the normalization of the cookie names (javiereguiluz) - * feature #30027 [FrameworkBundle] Add sid_length and sid_bits_per_character session ini options in session configuration (XuruDragon) - * feature #30075 [DependencyInjection] Added information about deprecated aliases in debug:autowiring (XuruDragon) - * feature #30024 [Debug] Display more details in the simple error page of Debug (javiereguiluz) - * feature #30052 [Security] Replace serialization API (renanbr) - * feature #27898 [Yaml] Fixed invalid Parser behavior (guiguiboy) - * feature #29753 [Console] Add an iterate method to the ProgressBar class (jvasseur) - * feature #29999 [PropertyAccess] speed up accessing object properties (xabbuh) - * feature #29641 [Validator] NotBlank: add a new option to allow null values (dunglas) - * feature #28721 [Form] deprecate some options for single_text widgets (xabbuh) - * feature #29936 [Mime] Add a set of default content-types for some extensions (fabpot) - * feature #28865 [Routing] allow using compiled matchers and generators without dumping PHP code (nicolas-grekas) - * feature #29236 [Cache] deprecate all PSR-16 adapters, provide Psr16Cache instead (nicolas-grekas) - * feature #29958 introducing native php serialize() support for Messenger transport (weaverryan, xabbuh) - * feature #29861 [Form][TwigBridge] Add help_html (mpiot) - * feature #29968 [DI] Added support for deprecating aliases (j92, Renan) - * feature #29850 [FrameworkBundle] xliff-version option to translation update command (andrewwro) - * feature #29896 [Mime] Add the component (fabpot) - * feature #29862 Add block prefix to csrf token field (alexander-schranz) - * feature #29881 [BrowserKit] Various changes to the Response class (fabpot) - * feature #29813 [FrameworkBundle] Remove ControllerTrait::isFormValid() (lyrixx) - * feature #29148 Load original file metadata when loading Xliff 1.2 files (eternoendless) - * feature #29840 [FrameworkBundle] pass project dir into the assets install command (xabbuh) - * feature #29821 [VarDumper] add caster for OpenSSL X.509 resources (nicolas-grekas) - * feature #29781 [DI] Add trim env processor (ogizanagi) - * feature #28902 [Debug] Detect virtual methods using @method (ro0NL) - * feature #29780 [Profiler] Still show locale and fallback locale even if no trans used (ogizanagi) - * feature #29680 [Form] Add new block_prefix option for an easy form theming (yceruto) - * feature #29528 [DebugBundle] Added 'theme' option to change the color of dump() when rendered inside templates (dem3trio) - * feature #24576 [FrameworkBundle] Added `ControllerTrait::isFormValid` (lyrixx) - * feature #29483 [HttpKernel] Set the default locale early (thewilkybarkid) - * feature #29186 [HttpKernel] Increase priority of AddRequestFormatsListener (thewilkybarkid) - * feature #29658 [Validator] Choices constraint improvement (nikophil) - * feature #29283 [Serializer] CsvEncoder no header option (encode / decode) (redecs) - * feature #29718 [PHPUnit bridge] Bump php version of PHPUnit-bridge (gmponos) - * feature #29599 [Routing] Allow force-generation of trailing parameters using eg "/exports/news.{!_format}" (zavulon) - * feature #29613 [VarDumper] Use hyperlinks in CliDescriptor (ogizanagi) - * feature #28581 [DomCrawler] return empty string on `Crawler::text()` and `Crawler::html()` instead of an exception (respinoza) - * feature #29286 [WebProfilerBundle] Enable translation filters (ro0NL) - * feature #29517 [Hackday][Messenger] Add an alias for transport.symfony_serializer so SerializerInterface can be autowired (karser) - * feature #29108 [DI] compute autowiring error messages lazily (nicolas-grekas) - * feature #29235 [VarDumper] add support for links in CliDumper (nicolas-grekas) - * feature #29541 [FrameworkBundle] Stop calling Kernel::boot() twice in cli (chalasr) - * feature #28931 [PhpUnitBridge] Added ClassExistsMock (ro0NL) - * feature #29504 [Validator] Add support for UATP card validation (raulfraile) - * feature #29168 [Console] Add hyperlinks support (ostrolucky) - * feature #29439 [PhpUnitBridge] install PHPUnit 7 on PHP 7.1 and fix requir. for PHPUnit 6 (gregurco) - * feature #29452 [Form] Shortcut debug:form for partial type name (ro0NL) - * feature #28954 [Debug] Mark ErrorHandler and ExceptionHandler classes as final (fancyweb) - * feature #28479 [Validator] Checking a BIC along with an IBAN (sylfabre) - * feature #28858 [DI] Deprecated using env vars with cannotBeEmpty() (ro0NL) - * feature #28976 [DI] Add a "default" EnvProcessor (jderusse) - * feature #29127 [DomCrawler] Added return of element name in `extract()` method (andrey-helldar) - * feature #29145 [Workflow] Trigger `entered` event for subject entering in the Workflow for the first time (lyrixx) - diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md deleted file mode 100644 index 29cc4c6552b90..0000000000000 --- a/CHANGELOG-4.4.md +++ /dev/null @@ -1,1504 +0,0 @@ -CHANGELOG for 4.4.x -=================== - -This changelog references the relevant changes (bug and security fixes) done -in 4.4 minor versions. - -To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash -To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1 - -* 4.4.39 (2022-03-05) - - * bug #45631 [HttpFoundation] Fix PHP 8.1 deprecation in `Response::isNotModified` (HypeMC) - * bug #45610 [HttpKernel] Guard against bad profile data (nicolas-grekas) - * bug #45532 Fix deprecations on PHP 8.2 (nicolas-grekas) - * bug #45595 [FrameworkBundle] Fix resetting container between tests (nicolas-grekas) - * bug #45585 [HttpClient] fix checking for unset property on PHP <= 7.1.4 (nicolas-grekas) - * bug #45583 [WebProfilerBundle] Fixes HTML syntax regression introduced by #44570 (xavismeh) - -* 4.4.38 (2022-02-28) - - * bug #44570 [WebProfilerBundle] add nonces to profiler (garak) - * bug #44839 MailerInterface: failed exception contract when enabling messenger (Giorgio Premi) - * bug #45529 [DependencyInjection] Don't reset env placeholders during compilation (nicolas-grekas) - * bug #45527 [HttpClient] Fix overriding default options with null (nicolas-grekas) - * bug #45531 [Serializer] Fix passing null to str_contains() (Erwin Dirks) - * bug #42458 [Validator][Tests] Fix AssertingContextualValidator not throwing on remaining expectations (fancyweb) - * bug #45496 [VarDumper] Fix dumping mysqli_driver instances (nicolas-grekas) - * bug #45495 [HttpFoundation] Fix missing ReturnTypeWillChange attributes (luxemate) - * bug #45482 [Cache] Add missing log when saving namespace (developer-av) - * bug #45479 [HttpKernel] Reset services between requests performed by KernelBrowser (nicolas-grekas) - * bug #44650 [Serializer] Make document type nodes ignorable (boenner) - * bug #45469 [SecurityBundle] fix autoconfiguring Monolog's ProcessorInterface (nicolas-grekas) - * bug #45414 [FrameworkBundle] KernelTestCase resets internal state on tearDown (core23) - * bug #45460 [Intl] fix wrong offset timezone PHP 8.1 (Lenny4) - * bug #45462 [HttpKernel] Fix extracting controller name from closures (nicolas-grekas) - * bug #45424 [DependencyInjection] Fix type binding (sveneld) - * bug #44259 [Security] AccountStatusException::$user should be nullable (Cantepie) - * bug #45323 [Serializer] Fix ignored callbacks in denormalization (benjaminmal) - * bug #45399 [FrameworkBundle] Fix sorting bug in sorting of tagged services by priority (Ahummeling) - * bug #45338 [Mailer] Fix string-cast of exceptions thrown by authenticator in EsmtpTransport (wikando-ck) - * bug #45339 [Cache] fix error handling when using Redis (nicolas-grekas) - * bug #45281 [Cache] Fix connecting to Redis via a socket file (alebedev80) - * bug #45289 [FrameworkBundle] Fix log channel of TagAwareAdapter (fancyweb) - * bug #45306 [PropertyAccessor] Add missing TypeError catch (b1rdex) - * bug #44868 [DependencyInjection][FrameworkBundle] Fix using PHP 8.1 enum as parameters (ogizanagi) - * bug #45261 [HttpClient] Fix Content-Length header when possible (nicolas-grekas) - * bug #45258 [DependencyInjection] Don't dump polyfilled classes in preload script (nicolas-grekas) - * bug #38534 [Serializer] make XmlEncoder stateless thus reentrant (connorhu) - * bug #42253 [Form] Do not fix URL protocol for relative URLs (bogkonstantin) - * bug #45256 [DomCrawler] ignore bad charsets (nicolas-grekas) - * bug #45255 [PropertyAccess] Fix handling of uninitialized property of parent class (filiplikavcan) - * bug #45204 [Validator] Fix minRatio and maxRatio when getting rounded (alexander-schranz) - * bug #45240 [Console] Revert StringInput bc break from #45088 (bobthecow) - -* 4.4.37 (2022-01-28) - - * bug #44939 [Form] UrlType should not add protocol to emails (GromNaN) - * bug #43149 Silence warnings during tty detection (neclimdul) - * bug #45181 [Console] Fix PHP 8.1 deprecation in ChoiceQuestion (BrokenSourceCode) - * bug #45140 [Yaml] Making the parser stateless (mamazu) - * bug #45103 [Process] Avoid calling fclose on an already closed resource (Seldaek) - * bug #45088 [Console] fix parsing escaped chars in StringInput (nicolas-grekas) - * bug #45096 [Cache] Throw exception if incompatible version of psr/simple-cache is used (colinodell) - * bug #45063 [DependencyInjection] remove arbitratry limitation to exclude inline services from bindings (nicolas-grekas) - * bug #44986 [DependencyInjection] copy synthetic status when resolving child definitions (kbond) - * bug #45073 [HttpClient] Fix Failed to open stream: Too many open files (adrienfr) - * bug #45053 [Console] use STDOUT/ERR in ConsoleOutput to save opening too many file descriptors (nicolas-grekas) - * bug #45029 [Cache] Set mtime of cache files 1 year into future if they do not expire (Blacksmoke16) - * bug #45012 [DoctrineBridge] Fix invalid guess with enumType (jderusse) - * bug #45015 [HttpClient] fix resetting DNS/etc when calling CurlHttpClient::reset() (nicolas-grekas) - * bug #44890 [HttpClient] Remove deprecated usage of `GuzzleHttp\Promise\queue` (GrahamCampbell) - * bug #45002 [PropertyAccess] Fix handling of uninitialized property of anonymous class (filiplikavcan) - * bug #44979 [DependencyInjection] Add iterable to possible binding type (vladimir.panivko) - * bug #44976 [FrameworkBundle] Avoid calling rtrim(null, '/') in AssetsInstallCommand (pavol-tk, GromNaN) - * bug #44879 [DependencyInjection] Ignore argument type check in CheckTypeDeclarationsPass if it's a Definition with a factory (fancyweb) - * bug #44931 Allow a zero time-limit for messenger:consume (fritzmg) - * bug #44932 [DependencyInjection] Fix nested env var with resolve processor (Laurent Moreau) - * bug #44912 [Console] Allow OutputFormatter::escape() to be used for escaping URLs used in (Seldaek) - * bug #44878 [HttpClient] Turn negative timeout to a very long timeout (fancyweb) - * bug #44854 [Validator] throw when Constraint::_construct() has not been called (nicolas-grekas) - -* 4.4.36 (2021-12-29) - - * bug #44838 [DependencyInjection][HttpKernel] Fix enum typed bindings (ogizanagi) - * bug #44826 [HttpKernel] Do not attempt to register enum arguments in controller service locator (ogizanagi) - * bug #44820 [Cache] Don't lock when doing nested computations (nicolas-grekas) - * bug #44807 [Messenger] fix Redis support on 32b arch (nicolas-grekas) - * bug #44759 [HttpFoundation] Fix notice when HTTP_PHP_AUTH_USER passed without pass (Vitali Tsyrkin) - * bug #44799 [Cache] fix compat with apcu < 5.1.10 (nicolas-grekas) - * bug #44732 [Mime] Relaxing in-reply-to header validation (ThomasLandauer) - * bug #44728 [Mime] Fix encoding filenames in multipart/form-data (nicolas-grekas) - * bug #44710 [DependencyInjection] fix linting callable classes (nicolas-grekas) - * bug #44639 [DependencyInjection] Cast tag attribute value to string (ruudk) - * bug #44473 [Validator] Restore default locale in ConstraintValidatorTestCase (rodnaph) - * bug #44577 [Cache] Fix proxy no expiration to the Redis (Sergey Belyshkin) - * bug #44669 [Cache] disable lock on CLI (nicolas-grekas) - * bug #44537 [Config] In XmlUtils, avoid converting from octal every string starting with a 0 (alexandre-daubois) - * bug #44625 [HttpClient] fix monitoring responses issued before reset() (nicolas-grekas) - * bug #44623 [HttpClient] Fix dealing with "HTTP/1.1 000 " responses (nicolas-grekas) - * bug #44601 [HttpClient] Fix closing curl-multi handle too early on destruct (nicolas-grekas) - * bug #44571 [HttpClient] Don't reset timeout counter when initializing requests (nicolas-grekas) - * bug #44479 [HttpClient] Double check if handle is complete (Nyholm) - * bug #44418 [DependencyInjection] Resolve ChildDefinition in AbstractRecursivePass (fancyweb) - * bug #43164 [FrameworkBundle] Fix cache pool configuration with one adapter and one provider (fancyweb) - * bug #44538 [Process] fixed uppercase ARGC and ARGV should also be skipped (rbaarsma) - * bug #44438 [HttpClient] Fix handling thrown \Exception in \Generator in MockResponse (fancyweb) - * bug #44502 [HttpFoundation] do not call preg_match() on null (xabbuh) - * bug #44467 [Console] Fix parameter types for `ProcessHelper::mustRun()` (derrabus) - * bug #44399 Prevent infinite nesting of lazy `ObjectManager` instances when `ObjectManager` is reset (Ocramius) - * bug #44375 [DoctrineBridge] fix calling get_class on non-object (kbond) - * bug #44361 [HttpClient] Fix handling error info in MockResponse (fancyweb) - * bug #43876 [Validator] Fix validation for single level domains (HypeMC) - * bug #44327 [Debug][ErrorHandler] Increased the reserved memory from 10k to 32k (sakalys) - * bug #44261 [Process] intersect with getenv() in case-insensitive manner to get default envs (stable-staple) - * bug #44295 [Serializer] fix support for lazy/unset properties (nicolas-grekas) - * bug #44269 [DoctrineBridge] Revert " add support for the JSON type" (dunglas) - -* 4.4.35 (2021-11-24) - - * security #cve-2021-41270 [Serializer] Use single quote to escape formulas (jderusse) - * bug #44232 [Cache] fix connecting to local Redis sockets (nicolas-grekas) - * bug #44204 [HttpClient] fix closing curl multi handle when destructing client (nicolas-grekas) - * bug #44208 [Process] exclude argv/argc from possible default env vars (nicolas-grekas) - -* 4.4.34 (2021-11-22) - - * bug #44188 [VarExporter] fix exporting declared but unset properties when __sleep() is implemented (nicolas-grekas) - * bug #44119 [HttpClient][Mime] Add correct IDN flags for IDNA2008 compliance (j-bernard) - * bug #44131 [Yaml] properly parse quoted strings tagged with !!str (xabbuh) - * bug #42323 [TwigBridge] do not merge label classes into expanded choice labels (xabbuh) - * bug #44121 [Serializer] fix support for lazy properties (nicolas-grekas) - * bug #44111 [Serializer] fix support for unset properties on PHP < 7.4 (nicolas-grekas) - * bug #44070 [Process] intersect with getenv() to populate default envs (nicolas-grekas) - * bug #44043 [Cache] fix dbindex Redis (a1812) - * bug #44042 Fix DateIntervalToStringTransformer::transform() doc (BenMorel) - * bug #44034 [Yaml] don't try to replace references in quoted strings (xabbuh) - * bug #44028 [ErrorHandler] Fix FlattenException::setPrevious argument typing (welcoMattic) - * bug #44012 [DependencyInjection] fix inlining when non-shared services are involved (nicolas-grekas) - * bug #44002 [Cache] Fix Memory leak (a1812) - * bug #43981 [FrameworkBundle] fix registering late resettable services (nicolas-grekas) - * bug #43988 [DoctrineBridge] add support for the JSON type (dunglas) - * bug #43987 [PhpUnitBridge] Fix Uncaught ValueError (dunglas) - * bug #43961 [HttpClient] Curl http client has to reinit curl multi handle on reset (rmikalkenas) - * bug #43922 [DependencyInjection] only allow `ReflectionNamedType` for `ServiceSubscriberTrait` (kbond) - * bug #43901 [SecurityBundle] Default access_decision_manager.strategy option with merge (biozshock) - * bug #43909 [VarExporter] escape unicode chars involved in directionality (nicolas-grekas) - * bug #43867 [VarDumper] Make dumping DateInterval instances timezone-independent (derrabus) - * bug #43096 [Messenger] Use `TransportMessageIdStamp` in `InMemoryTransport` allows retrying (alexndlm) - * bug #43501 [HttpKernel] fix ErrorException in CacheWarmerAggregate (Ahummeling) - * bug #42361 [Translation] correctly handle intl domains with TargetOperation (acran) - * bug #43834 [Inflector] Fix inflector for "zombies" (acodispo) - * bug #43267 [Config] Fix signature generation with nested attributes on PHP 8.1 (agustingomes) - -* 4.4.33 (2021-10-29) - - * bug #43798 [Dotenv] Duplicate $_SERVER values in $_ENV if they don't exist (fancyweb) - * bug #43799 [PhpUnitBridge] fix symlink to bridge in docker by making its path relative (nicolas-grekas) - * bug #43781 [Messenger] Fix `TraceableMessageBus` implementation so it can compute caller even when used within a callback (Ocramius) - * bug #43655 [VarDumper] Fix dumping twig templates found in exceptions (event15) - * bug #43484 [Messenger] Fix Redis Transport when username is empty (villfa) - * bug #43568 [Messenger] fix: TypeError in PhpSerializer::encode() (dsech) - * bug #43591 [Config] Fix files sorting in GlobResource (lyrixx) - * bug #43569 [HttpClient] fix collecting debug info on destruction of CurlResponse (nicolas-grekas) - * bug #43545 [DependencyInjection] fix "url" env var processor (nicolas-grekas) - * bug #43413 [VarDumper] Fix error with uninitialized XMLReader (villfa) - * bug #43388 [Validator] Fixes URL validation for single-char subdomains (DfKimera) - * bug #43333 [HttpClient] fix missing kernel.reset tag on TraceableHttpClient services (nicolas-grekas) - * bug #43302 [Cache] Commit items implicitly only when deferred keys are requested (Sergey Belyshkin) - * bug #43330 [Cache][Lock] fix SQLSRV throws for method_exists() (GDmac) - * bug #43270 [VarDumper] Fix handling of "new" in initializers on PHP 8.1 (nicolas-grekas) - * bug #43277 [DependencyInjection] fix support for "new" in initializers on PHP 8.1 (nicolas-grekas) - * bug #43243 [HttpClient] accept headers when CURLE_RECV_ERROR is received before the content (nicolas-grekas) - * bug #43205 [Serializer] Fix denormalizing XML array with empty body (4.4) (alexandre-daubois) - -* 4.4.32 (2021-09-28) - - * Fix subtree split issues - -* 4.4.31 (2021-09-28) - - * bug #43158 [Cache] Fix invalidating tags on Redis <5 (wouterj) - * bug #43179 [Ldap] Fix `resource` type checks & docblocks on PHP 8.1 (chalasr) - * bug #43137 [FrameworkBundle] Avoid secrets:decrypt-to-local command to fail (noniagriconomie) - * bug #43171 [VarDumper] fix dumping typed references from properties (nicolas-grekas) - * bug #43124 [Messenger] [Redis] Allow authentication with user and password (GaryPEGEOT) - * bug #39350 [FrameworkBundle] Remove translation data_collector BEFORE adding it to profiler (l-vo) - * bug #43115 [DependencyInjection] Fix iterator in ServiceConfigurator (jderusse) - * bug #43031 [Form] Do not trim unassigned unicode characters (simonberger) - * bug #43058 [WebProfilerBundle] Fix displaying certain configs (HypeMC) - * bug #43022 [PhpUnitBridge] Track unsilenced deprecations only for userland (nicolas-grekas) - * bug #42976 [Mime] Allow array as input for RawMessage (derrabus) - * bug #42098 [PropertyInfo] Support for intersection types (derrabus) - * bug #42904 [Cache] Make sure PdoAdapter::prune() always returns a bool (derrabus) - * bug #42896 [HttpClient] Fix handling timeouts when responses are destructed (nicolas-grekas) - * bug #42835 [Cache] Fix implicit float to int cast (derrabus) - * bug #42831 [Mime] Update mime types (fabpot) - * bug #42830 [HttpKernel] Fix empty timeline in profiler (nicodmf) - * bug #42819 Fix tests failing with DBAL 3 (derrabus) - -* 4.4.30 (2021-08-30) - - * bug #42753 Cast ini_get to an integer to match expected type (natewiebe13) - * bug #42345 [Messenger] Remove indices in messenger table on MySQL to prevent deadlocks while removing messages when running multiple consumers (jeroennoten) - * bug #40744 allow null for framework.translator.default_path (SimonHeimberg) - * bug #39856 [DomCrawler] improve failure messages of the CrawlerSelectorTextContains constraint (xabbuh) - * bug #40545 [HttpFoundation] Fix isNotModified determination logic (ol0lll) - * bug #42368 [FrameworkBundle] Fall back to default configuration in debug:config and consistently resolve parameter values (herndlm) - * bug #41684 Fix Url Validator false positives (sidz) - * bug #42576 [Translation] Reverse fallback locales (ro0NL) - * bug #42628 [PropertyInfo] Support for the `never` return type (derrabus) - * bug #42585 [ExpressionLanguage] [Lexer] Remove PHP 8.0 polyfill (nigelmann) - * bug #42621 [Security] Don't produce TypeErrors for non-string CSRF tokens (derrabus) - * bug #42365 [Cache] Do not add namespace argument to `NullAdapter` in `CachePoolPass` (olsavmic) - * bug #42331 [HttpKernel] always close open stopwatch section after handling `kernel.request` events (xabbuh) - * bug #42260 Fix return types for PHP 8.1 (derrabus) - * bug #42341 [Validator] Update MIR card scheme (ossinkine) - -* 4.4.29 (2021-07-29) - - * bug #42307 [Mailer] Fixed decode exception when sendgrid response is 202 (rubanooo) - * bug #42296 [Dotenv][Yaml] Remove PHP 8.0 polyfill (derrabus) - * bug #42289 [HttpFoundation] Fixed type mismatch (Toflar) - -* 4.4.28 (2021-07-27) - - * bug #42270 [WebProfilerBundle] [WebProfiler] "empty" filter bugfix. Filter with name "empty" is not … (luzrain) - -* 4.4.27 (2021-07-26) - - * bug #42212 [Lock] Handle lock with long key (jderusse) - * bug #42223 [Debug][ErrorHandler] Do not use the php80 polyfill (nicolas-grekas) - * bug #42207 [Console] fix table setHeaderTitle without headers (a1812) - * bug #42130 [Translation] fix fallback to Locale::getDefault() (nicolas-grekas) - * bug #42184 [Mailer] Make sure Http TransportException is not leaking (Nyholm) - * bug #42150 [Form] Fix 'invalid_message' use in multiple ChoiceType (alexandre-daubois) - * bug #42174 Indicate compatibility with psr/log 2 and 3 (derrabus) - * bug #42112 [HttpFoundation] fix FileBag under PHP 8.1 (alexpott) - * bug #42131 [PhpUnitBridge] Fix composer resolution on Windows (Rainrider) - * bug #42097 [DependencyInjection] Support for intersection types (derrabus) - * bug #42114 [HttpFoundation] Fix return types of SessionHandler::gc() (derrabus) - * bug #42099 [VarDumper] Support for intersection types (derrabus) - * bug #42011 [Cache] Support decorated Dbal drivers in PdoAdapter (Jeroeny) - * bug #42068 Add a Special Case for Translating Choices in en_US_POSIX (chrisguitarguy) - * bug #42074 Fix ctype_digit deprecation (alexpott) - * bug #42084 [WebProfilerBundle] Fix the values of some CSS properties (javiereguiluz) - * bug #42079 [FrameworkBundle] Fixed file operations in Sodium vault seal (javiereguiluz) - * bug #42054 [DoctrineBridge] fix setting default mapping type to attribute/annotation on php 8/7 respectively (nicolas-grekas) - * bug #42049 [TwigBridge] do not render the same label id attribute twice (xabbuh) - * bug #42032 [HttpKernel] recover from failed deserializations (xabbuh) - * bug #41990 [Lock] fix derivating semaphore from key (nicolas-grekas) - * bug #40529 [Translation] Missing translations from traits (insekticid) - * bug #41384 Fix SkippedTestSuite (jderusse) - * bug #41966 [Console] Revert "bug #41952 fix handling positional arguments" (chalasr, nicolas-grekas) - * bug #41905 [EventDispatcher] Correct the called event listener method case (JJsty1e) - * bug #41952 [Console] fix handling positional arguments (nicolas-grekas) - * bug #41887 [PhpUnitBridge] Fix deprecation handler with PHPUnit 10 (YaFou) - -* 4.4.26 (2021-06-30) - - * bug #41893 [Filesystem] Workaround cannot dumpFile into "protected" folders on Windows (arnegroskurth) - * bug #41665 [HttpKernel] Keep max lifetime also when part of the responses don't set it (mpdude) - * bug #41760 [ErrorHandler] fix handling buffered SilencedErrorContext (nicolas-grekas) - * bug #41807 [HttpClient] fix Psr18Client when allow_url_fopen=0 (nicolas-grekas) - * bug #40857 [DependencyInjection] Add support of PHP enumerations (alexandre-daubois) - * bug #41767 [Config] fix tracking default values that reference the parent class (nicolas-grekas) - * bug #41768 [DependencyInjection] Fix binding "iterable $foo" when using the PHP-DSL (nicolas-grekas) - * bug #41793 [Cache] handle prefixed redis connections when clearing pools (nicolas-grekas) - * bug #41804 [Cache] fix eventual consistency when using RedisTagAwareAdapter with a cluster (nicolas-grekas) - * bug #41773 [Cache] Disable locking on Windows by default (nicolas-grekas) - * bug #41655 [Mailer] fix encoding of addresses using SmtpTransport (dmaicher) - * bug #41663 [HttpKernel] [HttpCache] Keep s-maxage=0 from ESI sub-responses (mpdude) - * bug #41701 [VarDumper] Fix tests for PHP 8.1 (alexandre-daubois) - * bug #41795 [FrameworkBundle] Replace var_export with VarExporter to use array short syntax in secrets list files (alexandre-daubois) - * bug #41779 [DependencyInjection] throw proper exception when decorating a synthetic service (nicolas-grekas) - * bug #41776 [ErrorHandler] [DebugClassLoader] Do not check Phake mocks classes (adoy) - * bug #41780 [PhpUnitBridge] fix handling the COMPOSER_BINARY env var when using simple-phpunit (Taluu) - * bug #41670 [HttpFoundation] allow savePath of NativeFileSessionHandler to be null (simon.chrzanowski) - * bug #41644 [Config] fix tracking attributes in ReflectionClassResource (nicolas-grekas) - * bug #41621 [Process] Fix incorrect parameter type (bch36) - * bug #41624 [HttpClient] Revert bindto workaround for unaffected PHP versions (derrabus) - * bug #41549 [Security] Fix opcache preload with alias classes (jderusse) - * bug #41491 [Serializer] Do not allow to denormalize string with spaces only to valid a DateTime object (sidz) - * bug #41386 [Console] Escape synopsis output (jschaedl) - * bug #41495 [HttpFoundation] Add ReturnTypeWillChange to SessionHandlers (nikic) - -* 4.4.25 (2021-06-01) - - * bug #41000 [Form] Use !isset for checks cause this doesn't falsely include 0 (Kai Dederichs) - * bug #41407 [DependencyInjection] keep container.service_subscriber tag on the decorated definition (xabbuh) - * bug #40866 [Filesystem] fix readlink() for Windows (a1812) - * bug #41394 [Form] fix support for years outside of the 32b range on x86 arch (nicolas-grekas) - * bug #39847 [Messenger] Fix merging PrototypedArrayNode associative values (svityashchuk) - * bug #41346 [WebProfilerBundle] Wrapping exception js in Sfjs check and also loading base_js Sfjs if needed (weaverryan) - -* 4.4.24 (2021-05-19) - - * security #cve-2021-21424 [Security\Core] Fix user enumeration via response body on invalid credentials (chalasr) - * bug #41230 [FrameworkBundle][Validator] Fix deprecations from Doctrine Annotations+Cache (derrabus) - * bug #41240 Fixed deprecation warnings about passing null as parameter (derrabus) - * bug #41241 [Finder] Fix gitignore regex build with "**" (mvorisek) - * bug #41224 [HttpClient] fix adding query string to relative URLs with scoped clients (nicolas-grekas) - * bug #41233 [DependencyInjection][ProxyManagerBridge] Don't call class_exists() on null (derrabus) - * bug #41210 [Console] Fix Windows code page support (orkan) - -* 4.4.23 (2021-05-12) - - * security #cve-2021-21424 [Security][Guard] Prevent user enumeration (chalasr) - * bug #41176 [DependencyInjection] fix dumping service-closure-arguments (nicolas-grekas) - * bug #41168 WDT: Only load "Sfjs" if it is not present already (weaverryan) - * bug #41147 [Inflector][String] wrong plural form of words ending by "pectus" (makraz) - * bug #41160 [HttpClient] Don't prepare the request in ScopingHttpClient (nicolas-grekas) - * bug #40763 Fix/Rewrite .gitignore regex builder (mvorisek) - * bug #40917 [Config][DependencyInjection] Uniformize trailing slash handling (dunglas) - * bug #40699 [PropertyInfo] Make ReflectionExtractor correctly extract nullability (shiftby) - * bug #40874 [PropertyInfo] fix attribute namespace with recursive traits (soullivaneuh) - * bug #41099 [Cache] Check if phpredis version is compatible with stream parameter (nicolassing) - * bug #41072 [VarExporter] Add support of PHP enumerations (alexandre-daubois) - * bug #41105 [Inflector][String] Fixed singularize `edges` > `edge` (ruudk) - * bug #41075 [ErrorHandler] Skip "same vendor" ``@method`` deprecations for `Symfony\*` classes unless symfony/symfony is being tested (nicolas-grekas) - -* 4.4.22 (2021-05-01) - - * bug #40993 [Security] [Security/Core] fix checking for bcrypt (nicolas-grekas) - * bug #40923 [Yaml] expose references detected in inline notation structures (xabbuh) - * bug #40964 [HttpFoundation] Fixes for PHP 8.1 deprecations (jrmajor) - * bug #40514 [Yaml] Allow tabs as separators between tokens (bertramakers) - * bug #40882 [Cache] phpredis: Added full TLS support for RedisCluster (jackthomasatl) - * bug #40793 [DoctrineBridge] Add support for a driver type "attribute" (beberlei) - * bug #40807 RequestMatcher issue when `_controller` is a closure (Plopix) - * bug #40811 [PropertyInfo] Use the right context for methods defined in traits (colinodell) - * bug #40330 [SecurityBundle] Empty line starting with dash under "access_control" causes all rules to be skipped (monteiro) - * bug #40780 [Cache] Apply NullAdapter as Null Object (roukmoute) - * bug #40740 [Cache][FrameworkBundle] Fix logging for TagAwareAdapter (fancyweb) - * bug #40755 [Routing] Better inline requirements and defaults parsing (Foxprodev) - * bug #40754 [PhpUnitBridge] Fix phpunit symlink on Windows (johnstevenson) - * bug #40707 [Yaml] Fixed infinite loop when parser goes through an additional and invalid closing tag (alexandre-daubois) - * bug #40679 [Debug][ErrorHandler] Avoid warning with Xdebug 3 with develop mode disabled (Jean85) - * bug #40702 [HttpClient] allow CurlHttpClient on Windows (n0rbyt3) - * bug #40503 [Yaml] fix parsing some block sequences (a1812) - * bug #40610 Fixed bugs found by psalm (Nyholm) - * bug #40603 [Config] Fixed support for nodes not extending BaseNode (Nyholm) - * bug #40645 [FrameworkBundle] Dont store cache misses on warmup (Nyholm) - * bug #40629 [DependencyInjection] Fix "url" env var processor behavior when the url has no path (fancyweb) - * bug #40655 [Cache] skip storing failure-to-save as misses in ArrayAdapter (nicolas-grekas) - * bug #40522 [Serializer] Allow AbstractNormalizer to use null for non-optional nullable constructor parameters without default value (Pierre Rineau) - * bug #40595 add missing queue_name to find(id) in doctrine messenger transport (monteiro) - -* 4.4.21 (2021-03-29) - - * bug #40598 [Form] error if the input string couldn't be parsed as a date (xabbuh) - * bug #40587 [HttpClient] fix using stream_copy_to_stream() with responses cast to php streams (nicolas-grekas) - * bug #40510 [Form] IntegerType: Always use en for IntegerToLocalizedStringTransformer (Warxcell) - * bug #40593 Uses the correct assignment action for console options depending if they are short or long (topikito) - * bug #40535 [HttpKernel] ConfigDataCollector to return known data without the need of a Kernel (topikito) - * bug #40552 [Translation] Fix update existing key with existing +int-icu domain (Alexis) - * bug #40537 [Security] Handle properly 'auto' option for remember me cookie security (fliespl) - * bug #40506 [Validator] Avoid triggering the autoloader for user-input values (Seldaek) - * bug #40538 [HttpClient] remove using $http_response_header (nicolas-grekas) - * bug #40508 [PhpUnitBridge] fix reporting deprecations from DebugClassLoader (nicolas-grekas) - * bug #40348 [Console] Fix line wrapping for decorated text in block output (grasmash) - * bug #40499 [Inflector][String] Fixed pluralize "coupon" (Nyholm) - * bug #40494 [PhpUnitBridge] fix compat with symfony/debug (nicolas-grekas) - * bug #40453 [VarDumper] Adds support for ReflectionUnionType to VarDumper (Michael Nelson, michaeldnelson) - * bug #40460 Correctly clear lines for multi-line progress bar messages (grasmash) - * bug #40450 [Console] ProgressBar clears too many lines on update (danepowell) - * bug #40178 [FrameworkBundle] Exclude unreadable files when executing About command (michaljusiega) - * bug #40472 [Bridge\Twig] Add 'form-control-range' for range input type (Oviglo) - * bug #39866 [Mime] Escape commas in address names (YaFou) - * bug #40373 Check if templating engine supports given view (fritzmg) - * bug #39992 [Security] Refresh original user in SwitchUserListener (AndrolGenhald) - * bug #40446 [TwigBridge] Fix "Serialization of 'Closure'" error when rendering an TemplatedEmail (jderusse) - * bug #40425 [DoctrineBridge] Fix eventListener initialization when eventSubscriber constructor dispatch an event (jderusse) - * bug #40313 [FrameworkBundle] Fix PropertyAccess definition when not in debug (PedroTroller) - * bug #40417 [Form] clear unchecked choice radio boxes even if clear missing is set to false (xabbuh) - * bug #40388 [ErrorHandler] Added missing type annotations to FlattenException (derrabus) - * bug #40407 [TwigBridge] Allow version 3 of the Twig extra packages (derrabus) - * bug #39685 [Mailer][Mime][TwigBridge][Validator] Allow egulias/email-validator 3.x (derrabus) - * bug #40398 [FrameworkBundle] : Fix method name compare in ResolveControllerNameSubscriber (glensc) - * bug #39733 [TwigBridge] Render email once (jderusse) - * bug #40386 [DependencyInjection][Security] Backport psr/container 1.1/2.0 compatibility (derrabus) - -* 4.4.20 (2021-03-04) - - * bug #40318 [Translation] deal with indented heredoc/nowdoc tokens (xabbuh) - * bug #40350 [DependencyInjection] fix parsing calls of methods named "method" (xabbuh) - * bug #40316 [Serializer] zero parts can be omitted in date interval input (xabbuh) - * bug #40239 MockResponse total_time should not be simulated when provided (Pierrick VIGNAND) - * bug #40299 [Cache] Add server-commands support for Predis Replication Environments (DemigodCode) - * bug #40231 [HttpKernel] Configure `session.cookie_secure` earlier (tamcy) - * bug #40283 [Translation] Make `name` attribute optional in xliff2 (MarieMinasyan) - * bug #39599 [Cache] Fix Redis TLS scheme `rediss` for Redis connection (misaert) - * bug #40244 [Routing] fix conflict with param named class in attribute (nlhommet) - * bug #40273 [Cache] fix setting items' metadata on commit() (nicolas-grekas) - * bug #40258 [Form] Ignoring invalid forms from delete_empty behavior in CollectionType (yceruto) - * bug #40162 [Intl] fix Locale::getFallback() throwing exception on long $locale (AmirHo3ein13) - * bug #40208 [PropertyInfo] fix resolving self to name of the analyzed class (xabbuh) - * bug #40209 [WebLink] Escape double quotes in attributes values (fancyweb) - * bug #40192 [Console] fix QuestionHelper::getHiddenResponse() not working with space in project directory name (Yendric) - * bug #40175 [PropertyInfo]  use the right context for properties defined in traits (xabbuh) - * bug #40172 [Translation] Allow using dashes in locale when linting Xliff files (localheinz) - * bug #40187 [Console] Fix PHP 8.1 null error for preg_match flag (kylekatarnls) - * bug #39659 [Form] keep valid submitted choices when additional choices are submitted (xabbuh) - * bug #40188 [HttpFoundation] Fix PHP 8.1 null values (kylekatarnls) - * bug #40167 [DependencyInjection] Definition::removeMethodCall should remove all matching calls (ruudk) - * bug #40160 [PropertyInfo] fix extracting mixed type-hinted property types (xabbuh) - * bug #40040 [Finder] Use a lazyIterator to close files descriptors when no longer used (jderusse) - * bug #40135 [FrameworkBundle] Fix freshness checks with boolean parameters on routes (HypeMC) - * bug #40138 [FrameworkBundle] fix registering "annotations.cache" on the "container.hot_path" (nicolas-grekas) - * bug #40116 [FrameworkBundle][Translator] scan directories for translations sequentially (xabbuh) - * bug #40104 [HttpKernel] [Kernel] Silence failed deprecations logs writes (fancyweb) - * bug #40098 [DependencyInjection] fix tracking of changes to vendor/ dirs (nicolas-grekas) - * bug #39980 [Mailer][Mime] Update inline part names with newly generated ContentId (ddegentesh) - * bug #40043 [HttpFoundation] Setting `REQUEST_TIME_FLOAT` when constructing a Request object (ctasada) - * bug #40050 [FrameworkBundle][Translator] Fixed updating catalogue metadata from Intl domain (yceruto) - * bug #40089 [SecurityBundle] role_names variable instead of roles (wickedOne) - * bug #40042 [Doctrine] Restore priority for EventSubscribers (jderusse) - * bug #40066 [ErrorHandler] fix parsing return types in DebugClassLoader (nicolas-grekas) - * bug #40065 [ErrorHandler] fix handling messages with null bytes from anonymous classes (nicolas-grekas) - * bug #40067 [PhpUnitBridge] fix reporting deprecations when they come from DebugClassLoader (nicolas-grekas) - * bug #40060 fix validator when we have false returned by the current element of the iterator (FabienSalles) - * bug #40062 [Mime] Fix case-sensitive handling of header names (piku235) - * bug #40023 [Finder]  use proper keys to not override appended files (xabbuh) - * bug #40019 [ErrorHandler] Fix strpos error when trying to call a method without a name (Deuchnord) - * bug #40004 [Serializer] Prevent access to private properties without getters (julienfalque) - -* 4.4.19 (2021-01-27) - - * bug #38900 [Serializer] Exclude non-initialized properties accessed with getters (BoShurik) - * bug #39887 [Translator] fix handling plural for floating numbers (kylekatarnls) - * bug #39967 [Messenger] fix redis messenger options with dsn (Kleinast) - * bug #39970 [Messenger] Fix transporting non-UTF8 payloads by encoding them using base 64 (nicolas-grekas) - * bug #39909 [PhpUnitBridge] Allow relative path to composer cache (jderusse) - * bug #39944 [HttpKernel] Configure the ErrorHandler even when it is overriden (nicolas-grekas) - * bug #39932 [Console] [Command] Fix Closure code binding when it is a static anonymous function (fancyweb) - * bug #39880 [DoctrineBridge] Add username to UserNameNotFoundException (qurben) - * bug #39633 [HttpFoundation] Drop int return type from parseFilesize() (LukeTowers) - * bug #39889 [HttpClient] Add check for constant in Curl client (pierredup) - * bug #39886 [HttpFoundation] Revert #38614 and add assert to avoid regressions (BafS) - * bug #39858 Fix problem when SYMFONY_PHPUNIT_VERSION is empty string value (alexander-schranz) - * bug #39861 [DependencyInjection] Skip deprecated definitions in CheckTypeDeclarationsPass (chalasr) - * bug #39862 [Security] Replace message data in JSON security error response (wouterj) - * bug #39667 [DoctrineBridge] Take into account that indexBy="person_id" could be a db column name, for a referenced entity (victormacko) - * bug #39799 [DoctrineBridge] Fix circular loop with EntityManager (jderusse) - * bug #39821 [DependencyInjection] Don't trigger notice for deprecated aliases pointing to deprecated definitions (chalasr) - * bug #39816 [HttpFoundation] use atomic writes in MockFileSessionStorage (nicolas-grekas) - * bug #39735 [Serializer] Rename normalize param (VincentLanglet) - * bug #39797 Dont allow unserializing classes with a destructor (jderusse) - * bug #39743 [Mailer] Fix missing BCC recipients in SES bridge (jderusse) - * bug #39764 [Config]  fix handling float-like key attribute values (xabbuh) - * bug #39787 [Yaml] a colon followed by spaces exclusively separates mapping keys and values (xabbuh) - * bug #39788 [Cache] fix possible collision when writing tmp file in filesystem adapter (nicolas-grekas) - * bug #39794 Dont allow unserializing classes with a destructor - 4.4 (jderusse) - * bug #39747 [DependencyInjection] Support PHP 8 builtin types in CheckTypeDeclarationsPass (derrabus) - * bug #39738 [VarDumper] fix mutating $GLOBALS while cloning it (nicolas-grekas) - * bug #39746 [DependencyInjection] Fix InvalidParameterTypeException for function parameters (derrabus) - * bug #39681 [HttpFoundation] parse cookie values containing the equal sign (xabbuh) - * bug #39716 [DependencyInjection] do not break when loading schemas from network paths on Windows (xabbuh) - * bug #39703 [Finder] apply the sort callback on the whole search result (xabbuh) - * bug #39717 [TwigBridge] Remove full head content in HTML to text converter (pupaxxo) - * bug #39708 [WebProfilerBundle] take query and request parameters into account when matching routes (xabbuh) - * bug #39683 [Yaml] keep trailing newlines when dumping multi-line strings (xabbuh) - * bug #39670 [Form] disable error bubbling by default when inherit_data is configured (xabbuh) - * bug #39686 [Lock] Fix config merging in lock (jderusse) - * bug #39668 [Yaml] do not dump extra trailing newlines for multiline blocks (xabbuh) - * bug #39653 [Form] fix passing null $pattern to IntlDateFormatter (nicolas-grekas) - * bug #39598 [Messenger] Fix stopwach usage if it has been reset (lyrixx) - * bug #39631 [VarDumper] Fix display of nullable union return types (derrabus) - * bug #39629 [VarDumper] fixed displaying "mixed" as "?mixed" (nicolas-grekas) - * bug #39597 [Mailer] Handle failure when sending DATA (jderusse) - * bug #39610 [ProxyManagerBridge] fix PHP notice, switch to "friendsofphp/proxy-manager-lts" (nicolas-grekas) - -* 4.4.18 (2020-12-18) - - * bug #39531 [Mailer] Fix parsing Dsn with empty user/password (OskarStark) - * bug #39518 [Ldap] Incorrect determination of RelativeDistinguishedName for the "move" operation (astepin) - * bug #39476 [Lock] Prevent store exception break combined store (dzubchik) - * bug #39433 [Cache] fix setting "read_timeout" when using Redis (nicolas-grekas) - * bug #39420 [Cache] Prevent notice on case matching metadata trick (bastnic) - * bug #39203 [DI] Fix not working if only "default_index_method" used (malteschlueter) - * bug #39142 [Config] Stop treating multiline resources as globs (michaelKaefer) - * bug #39341 [Form] Fixed StringUtil::trim() to trim ZERO WIDTH SPACE (U+200B) and SOFT HYPHEN (U+00AD) (pmishev) - * bug #39334 [Config][TwigBundle] Fixed syntax error in config (Nyholm) - * bug #39196 [DI] Fix Xdebug 3.0 detection (vertexvaar) - * bug #39226 [PhpUnitBridge] Fix disabling DeprecationErrorHandler from PHPUnit configuration file (fancyweb) - * bug #39357 [FrameworkBundle] fix preserving some special chars in the query string (nicolas-grekas) - * bug #39271 [HttpFoundation] Fix TypeError: Argument 1 passed to JsonResponse::setJson() must be of the type string, object given (sidz) - * bug #39251 [DependencyInjection] Fix container linter for union types (derrabus) - * bug #39336 [Config] YamlReferenceDumper: No default value required for VariableNode with array example (Nyholm) - * bug #39333 [Form] do not apply the Valid constraint on scalar form data (lchrusciel, xabbuh) - * bug #39331 [PhpUnitBridge] Fixed PHPunit 9.5 compatibility (wouterj) - * bug #39220 [HttpKernel] Fix bug with whitespace in Kernel::stripComments() (ausi) - * bug #39252 [Mime] Leverage PHP 8's detection of CSV files (derrabus) - * bug #39313 [FrameworkBundle] TextDescriptor::formatControllerLink checked method… (fjogeleit) - * bug #39286 [HttpClient] throw clearer error when no scheme is provided (BackEndTea) - * bug #39267 [Yaml] fix lexing backslashes in single quoted strings (xabbuh) - * bug #39151 [DependencyInjection] Fixed incorrect report for private services if required service does not exist (Islam93) - * bug #39274 [Yaml] fix lexing mapping values with trailing whitespaces (xabbuh) - * bug #39270 [Inflector] Fix Notice when argument is empty string (moldman) - * bug #39247 [Security] remove return type definition in order to avoid type juggling (adeptofvoltron) - * bug #39223 [Console] Re-enable hyperlinks in Konsole/Yakuake (OndraM) - * bug #39241 [Yaml] fix lexing inline sequences/mappings with trailing whitespaces (Nyholm, xabbuh) - * bug #39243 [Filesystem] File existence check before calling unlink method (gechetspr) - -* 4.4.17 (2020-11-29) - - * bug #39166 [Messenger] Fix mssql compatibility for doctrine transport. (bill moll) - * bug #39211 [HttpClient] fix binding to network interfaces (nicolas-grekas) - * bug #39129 [DependencyInjection] Fix circular in DI with lazy + byContruct loop (jderusse) - * bug #39068 [DependencyInjection][Translator] Silent deprecation triggered by libxml_disable_entity_loader (jderusse) - * bug #39119 [Form] prevent duplicated error message for file upload limits (xabbuh) - * bug #39099 [Form] ignore the pattern attribute for textareas (xabbuh) - * bug #39154 [Yaml] fix lexing strings containing escaped quotation characters (xabbuh) - * bug #38597 [PhpUnitBridge] Fix qualification of deprecations triggered by the debug class loader (fancyweb) - * bug #39160 [Console] Use a partial buffer in SymfonyStyle (jderusse) - * bug #39168 [Console] Fix console closing tag (jderusse) - * bug #39155 [VarDumper] fix casting resources turned into objects on PHP 8 (nicolas-grekas) - * bug #39115 [HttpClient] don't fallback to HTTP/1.1 when HTTP/2 streams break (nicolas-grekas) - * bug #33763 [Yaml] fix lexing nested sequences/mappings (xabbuh) - * bug #39083 [Dotenv] Check if method inheritEnvironmentVariables exists (Chi-teck) - * bug #39094 [Ldap] Fix undefined variable $con (derrabus) - * bug #39091 [Config] Recheck glob brace support after GlobResource was serialized (wouterj) - * bug #39092 Fix critical extension when reseting paged control (jderusse) - * bug #38614 [HttpFoundation] Fix for virtualhosts based on URL path (mvorisek) - * bug #38387 [Validator] prevent hash collisions caused by reused object hashes (fancyweb, xabbuh) - * bug #38999 [DependencyInjection] autoconfigure behavior describing tags on decorators (xabbuh) - * bug #39058 [DependencyInjection] Fix circular detection with multiple paths (jderusse) - * bug #39059 [Filesystem] fix cleaning up tmp files when dumpFile() fails (nicolas-grekas) - * bug #38628 [DoctrineBridge] indexBy could reference to association columns (juanmiguelbesada) - * bug #39021 [DependencyInjection] Optimize circular collection by removing flattening (jderusse) - * bug #39031 [Ldap] Fix pagination (jderusse) - * bug #39038 [DoctrineBridge] also reset id readers (xabbuh) - * bug #39025 [DoctrineBridge] Fix DBAL deprecations in middlewares (derrabus) - * bug #38991 [Console] Fix ANSI when stdErr is not a tty (jderusse) - * bug #38980 [DependencyInjection] Fix circular reference with Factory + Lazy Iterrator (jderusse) - * bug #38971 [PhpUnitBridge] fix replaying skipped tests (nicolas-grekas) - * bug #38910 [HttpKernel] Fix session initialized several times (jderusse) - * bug #38882 [DependencyInjection] Improve performances in CircualReference detection (jderusse) - * bug #38950 [Process] Dont test TTY if there is no TTY support (Nyholm) - * bug #38921 [PHPUnitBridge] Fixed crash on Windows with PHP 8 (villfa) - * bug #38869 [SecurityBundle] inject only compatible token storage implementations for usage tracking (xabbuh) - * bug #38894 [HttpKernel] Remove Symfony 3 compatibility code (derrabus) - * bug #38895 [PhpUnitBridge] Fix wrong check for exporter in ConstraintTrait (alcaeus) - * bug #38879 [Cache] Fixed expiry could be int in ChainAdapter due to race conditions (phamviet) - * bug #38856 [Cache] Add missing use statement (fabpot) - -* 4.4.16 (2020-10-28) - - * bug #38713 [DI] Fix Preloader exception when preloading a class with an unknown parent/interface (rgeraads) - * bug #38647 [HttpClient] relax auth bearer format requirements (xabbuh) - * bug #38699 [DependencyInjection] Preload classes with union types correctly (derrabus) - * bug #38669 [Serializer] fix decoding float XML attributes starting with 0 (Marcin Kruk) - * bug #38680 [PhpUnitBridge] Support new expect methods in test case polyfill (alcaeus) - * bug #38681 [PHPUnitBridge] Support PHPUnit 8 and PHPUnit 9 in constraint compatibility trait (alcaeus) - * bug #38679 [PhpUnitBridge] Add missing exporter function for PHPUnit 7 (alcaeus) - * bug #38595 [TwigBridge] do not translate null placeholders or titles (xabbuh) - * bug #38635 [Cache] Use correct expiry in ChainAdapter (Nyholm) - * bug #38652 [Filesystem] Check if failed unlink was caused by permission denied (Nyholm) - * bug #38645 [PropertyAccess] forward the caught exception (xabbuh) - * bug #38604 [DoctrineBridge] indexBy does not refer to attributes, but to column names (xabbuh) - * bug #38606 [WebProfilerBundle] Hide debug toolbar in print view (jt2k) - * bug #38582 [DI] Fix Reflection file name with eval()\'d code (maxime-aknin) - * bug #38516 [HttpFoundation] Fix Range Requests (BattleRattle) - * bug #38553 [Lock] Reset Key lifetime time before we acquire it (Nyholm) - * bug #38551 Remove content-type check on toArray methods (jderusse) - * bug #38544 [DI] fix dumping env vars (nicolas-grekas) - * bug #38530 [HttpClient] fix reading the body after a ClientException (nicolas-grekas) - * bug #38510 [PropertyInfo] Support for the mixed type (derrabus) - * bug #38493 [HttpClient] Fix CurlHttpClient memory leak (HypeMC) - * bug #38456 [Cache] skip igbinary < 3.1.6 (nicolas-grekas) - * bug #38392 [Ldap] Bypass the use of `ldap_control_paged_result` on PHP >= 7.3 (lucasaba) - * bug #38444 [PhpUnitBridge] fix running parallel tests with phpunit 9 (nicolas-grekas) - * bug #38442 [VarDumper] fix truncating big arrays (nicolas-grekas) - * bug #38433 [Mime] Fix serialization of RawMessage (gilbertsoft) - -* 4.4.15 (2020-10-04) - - * bug #36291 [Lock] Fix StoreFactory to accept same DSN syntax as AbstractAdapter (Jontsa) - * bug #38390 [Serializer][Minor] Fix circular reference exception message (bad limit displayed) (l-vo) - * bug #38388 [HttpClient] Always "buffer" empty responses (nicolas-grekas) - * bug #38380 [Form] propagate validation groups to subforms (johanderuijter, xabbuh) - * bug #38377 Ignore more deprecations for Mockery mocks (fancyweb) - * bug #38375 [HttpClient] fix using proxies with NativeHttpClient (nicolas-grekas) - * bug #38372 [Routing] fix using !important and defaults/reqs in inline route definitions (nicolas-grekas) - * bug #38373 [ErrorHandler][DebugClassLoader] Do not check Mockery mocks classes (fancyweb) - * bug #38368 [HttpClient] Fix using https with proxies (bohanyang) - * bug #38350 [TwigBundle] Only remove kernel exception listener if twig is used (dmolineus) - * bug #38360 [BrowserKit] Cookie expiration at current timestamp (iquito) - * bug #38358 [Messenger] Fix redis connection error message (alexander-schranz) - * bug #38343 Revert "bug #38063 [FrameworkBundle] generate preload.php in src/ to make opcache.preload predictable" (nicolas-grekas) - * bug #38336 [PhpUnitBridge] Fixed class_alias() for PHPUnit\Framework\Error\Error (stevegrunwell) - -* 4.4.14 (2020-09-27) - - * bug #38248 [HttpClient] Allow bearer token with colon (stephanvierkant) - * bug #37837 [Form] Fix custom formats deprecation with HTML5 widgets (fancyweb) - * bug #38285 [Contracts][Translation] Optional Intl dependency (ro0NL) - * bug #38283 [Translator] Optional Intl dependency (ro0NL) - * bug #38271 [ErrorHandler] Escape JSON encoded log context (ro0NL) - * bug #38284 [Cache][Lock][Messenger] fix compatibility with Doctrine DBAL 3 (xabbuh) - * bug #38228 [Yaml Parser] Fix edge cases when parsing multiple documents (digilist) - * bug #38229 [Yaml] fix parsing comments not prefixed by a space (xabbuh) - * bug #38127 [Translator] Make sure a null locale is handled properly (jschaedl) - * bug #38221 [Cache] Allow cache tags to be objects implementing __toString() (lstrojny) - * bug #38212 [HttpKernel] Do not override max_redirects option in HttpClientKernel (dmolineus) - * bug #38215 [HttpClient] Support for CURLOPT_LOCALPORT (derrabus) - * bug #38202 [FrameworkBundle] Fix xsd definition which prevent to add more than one workflow metadata (l-vo) - * bug #38166 [Console] work around disabled putenv() (SenTisso) - * bug #38173 [HttpClient][HttpClientTrait] don't calculate alternatives if option is auth_ntlm (ybenhssaien) - * bug #38169 [PhpUnitBridge] Internal classes are not legacy (derrabus) - * bug #38156 [Cache] fix ProxyAdapter not persisting items with infinite expiration (dmaicher) - * bug #38148 [HttpClient] fail properly when the server replies with HTTP/0.9 (nicolas-grekas) - * bug #38131 [Validator] allow consumers to mock all methods (xabbuh) - * bug #38139 [DI] dump OS-indepent paths in the compiled container (nicolas-grekas) - * bug #38126 [Cache] Limit cache version character range (lstrojny) - * bug #38142 [FrameworkBundle] adopt src/.preload.php (nicolas-grekas) - * bug #38108 [Cache] Fix key encoding issue in Memcached adapter (lstrojny) - * bug #38122 [HttpClient] Fix Array to string conversion notice when parsing JSON error body with non-scalar detail property (emarref) - * bug #37097 DateTime validator support for trailing data (stefankleff) - * bug #38116 [Console] Silence warnings on sapi_windows_cp_set() call (chalasr) - * bug #38114 [Console] guard $argv + $token against null, preventing unnecessary exceptions (bilogic) - * bug #38094 [PhpUnitBridge] Skip internal classes in CoverageListenerTrait (sanmai) - * bug #38101 [VarExporter] unserialize() might throw an Exception on php 8 (derrabus) - * bug #38100 [ErrorHandler] Parse "x not found" errors correctly on php 8 (derrabus) - * bug #38099 Prevent parsing invalid octal digits as octal numbers (julienfalque) - * bug #38095 [Mailer] Remove unnecessary check for existing request (jschaedl) - * bug #38091 [DI] fix ContainerBuilder on PHP8 (nicolas-grekas) - * bug #38086 [HttpClient] with "bindto" with NativeHttpClient (nicolas-grekas) - * bug #38063 [FrameworkBundle] generate preload.php in src/ to make opcache.preload predictable (nicolas-grekas) - * bug #38080 [Console] Make sure $maxAttempts is an int or null (derrabus) - * bug #38075 esmtp error not being thrown properly (Anton Zagorskii) - * bug #38040 [Yaml Parser] fixed Parser to skip comments when inlining sequences (korve) - * bug #38073 [VarDumper] Fix caster for invalid SplFileInfo objects on php 8 (derrabus) - * bug #38071 [PhpUnitBridge] Adjust output parsing of CoverageListenerTrait for PHPUnit 9.3 (sanmai, derrabus) - * bug #38062 [DI] fix generating preload file when cache_dir is outside project_dir (nicolas-grekas) - * bug #38059 [Cache] Fix CacheCollectorPass with decorated cache pools (shyim) - * bug #38054 [PhpUnitBridge] CoverageListenerTrait update for PHPUnit 8.5/9.x (sanmai) - * bug #38049 [Debug] Parse "x not found" errors correctly on php 8 (derrabus) - * bug #38041 [PropertyInfo] Fix typed collections in PHP 7.4 (ndench) - * bug #37959 [PhpunitBridge] Fix deprecation type detection (when several autoload files are used) (l-vo) - -* 4.4.13 (2020-09-02) - - * security #cve-2020-15094 Remove headers with internal meaning from HttpClient responses (mpdude) - * bug #38024 [Console] Fix undefined index for inconsistent command name definition (chalasr) - * bug #38023 [DI] fix inlining of non-shared services (nicolas-grekas) - * bug #38020 [PhpUnitBridge] swallow deprecations (xabbuh) - * bug #38010 [Cache] Psr16Cache does not handle Proxy cache items (alex-dev) - * bug #37937 [Serializer] fixed fix encoding of cache keys with anonymous classes (michaelzangerle) - -* 4.4.12 (2020-08-31) - - * bug #37966 [HttpClient][MockHttpClient][DX] Throw when the response factory callable does not return a valid response (fancyweb) - * bug #37971 [PropertyInfo] Backport support for typed properties (PHP 7.4) (dunglas) - * bug #37970 [PhpUnitBridge] Polyfill new phpunit 9.1 assertions (phpfour) - * bug #37960 [PhpUnit] Add polyfill for assertMatchesRegularExpression() (dunglas) - * bug #37949 [Yaml] fix more numeric cases changing in PHP 8 (xabbuh) - * bug #37921 [Yaml] account for is_numeric() behavior changes in PHP 8 (xabbuh) - * bug #37912 [ExpressionLanguage] fix passing arguments to call_user_func_array() on PHP 8 (xabbuh) - * bug #37907 [Messenger] stop using the deprecated schema synchronizer API (xabbuh) - * bug #37900 [Mailer] Fixed mandrill api header structure (wulff) - * bug #37888 [Mailer] Reorder headers used to determine Sender (cvmiert) - * bug #37872 [Sendgrid-Mailer] Fixed envelope recipients on sendgridApiTransport (arendjantetteroo) - * bug #37860 [Serializer][ClassDiscriminatorMapping] Fix getMappedObjectType() when a discriminator child extends another one (fancyweb) - * bug #37853 [Validator] ensure that the validator is a mock object for backwards-compatibility (xabbuh) - * bug #36340 [Serializer] Fix configuration of the cache key (dunglas) - * bug #36810 [Messenger] Do not stack retry stamp (jderusse) - * bug #37849 [FrameworkBundle] Add missing mailer transports in xsd (l-vo) - * bug #37586 [ErrorHandler][DebugClassLoader] Add mixed and static return types support (fancyweb) - * bug #37845 [Serializer] Fix variadic support when using type hints (fabpot) - * bug #37841 [VarDumper] Backport handler lock when using VAR_DUMPER_FORMAT (ogizanagi) - * bug #37725 [Form] Fix Guess phpdoc return type (franmomu) - * bug #37771 Use PHPUnit 9.3 on php 8 (derrabus) - * bug #36140 [Validator] Add BC layer for notInRangeMessage when min and max are set (l-vo) - * bug #35843 [Validator] Add target guards for Composite nested constraints (ogizanagi) - * bug #37803 Fix for issue #37681 (Rav) - * bug #37744 [Yaml] Fix for #36624; Allow PHP constant as first key in block (jnye) - * bug #37767 [Form] fix mapping errors from unmapped forms (xabbuh) - * bug #37731 [Console] Table: support cells with newlines after a cell with colspan >= 2 (GMTA) - * bug #37791 Fix redis connect with empty password (alexander-schranz) - * bug #37790 Fix deprecated libxml_disable_entity_loader (fabpot) - * bug #37763 Fix deprecated libxml_disable_entity_loader (jderusse) - * bug #37774 [Console] Make sure we pass a numeric array of arguments to call_user_func_array() (derrabus) - * bug #37729 [FrameworkBundle] fail properly when the required service is not defined (xabbuh) - * bug #37701 [Serializer] Fix that it will never reach DOMNode (TNAJanssen) - * bug #37671 [Cache] fix saving no-expiry items with ArrayAdapter (philipp-kolesnikov) - * bug #37102 [WebProfilerBundle] Fix error with custom function and web profiler routing tab (JakeFr) - * bug #37560 [Finder] Fix GitIgnore parser when dealing with (sub)directories and take order of lines into account (Jeroeny) - * bug #37700 [VarDumper] Improve previous fix on light array coloration (l-vo) - * bug #37705 [Mailer] Added the missing reset tag to mailer.logger_message_listener (vudaltsov) - * bug #37697 [Messenger] reduce column length for MySQL 5.6 compatibility (xabbuh) - -* 4.4.11 (2020-07-24) - - * bug #37590 Allows RedisClusterProxy instance in Lock RedisStore (jderusse) - * bug #37583 [Mime] Fix EmailHeaderSame to make use of decoded value (evertharmeling) - * bug #37569 [Messenger] Allow same middleware to be used multiple times with different arguments (HypeMC) - * bug #37624 [Cache] Connect to RedisCluster with password auth (mforbak) - * bug #37635 [Cache] fix catching auth errors (nicolas-grekas) - * bug #37628 [Serializer] Support multiple levels of discriminator mapping (jeroennoten) - * bug #37572 [FrameworkBundle] set default session.handler alias if handler_id is not provided (Youssef BENHSSAIEN) - * bug #37607 Fix checks for phpunit releases on Composer 2 (colinodell) - * bug #37594 Use hexadecimal numerals instead of hexadecimals in strings to repres… (arekzb) - * bug #37576 [WebProfilerBundle] modified url generation to use absolute urls (smatyas) - * bug #36888 [Mailer] Fix mandrill raw http request setting from email/name (JohJohan) - * bug #37527 [Mailer] Fix reply-to functionality in the SendgridApiTransport (jt2k) - * bug #37581 [Mime] Fix compat with HTTP requests (fabpot) - * bug #37580 [Mime] Keep Sender full address when used by non-SMTP transports (fabpot) - * bug #37511 [DependencyInjection][Config] Use several placeholder unique prefixes for dynamic placeholder values (fancyweb) - * bug #37562 [Cache] Use the default expiry when saving (not when creating) items (philipp-kolesnikov) - * bug #37563 Fix DBAL deprecation (nicolas-grekas) - * bug #37521 [Form] Fix ChoiceType translation domain (VincentLanglet) - * bug #37550 [OptionsResolver] Fix force prepend normalizer (hason) - * bug #37520 [Form] silently ignore uninitialized properties when mapping data to forms (ph-fritsche) - * bug #37526 [Cache][Config] ensure compatibility with PHP 8 stack traces (xabbuh) - * bug #37513 [PhpUnitBridge] ExcludeList usage for PHPUnit 9.4 (gennadigennadigennadi) - * bug #37461 [Process] Fix Permission Denied error when writing sf_proc_00 lock files on Windows (JasonStephensTAMU) - * bug #37505 [Form] fix handling null as empty data (xabbuh) - * bug #37385 [Console] Fixes question input encoding on Windows (YaFou) - * bug #37491 [HttpClient] Fix promise behavior in HttplugClient (brentybh) - * bug #37469 [Console] always use stty when possible to ask hidden questions (nicolas-grekas) - * bug #37486 [HttpClient] fix parsing response headers in CurlResponse (nicolas-grekas) - * bug #37484 [HttpClient][CurlHttpClient] Fix http_version option usage (fancyweb) - * bug #37447 [Validator] fix validating lazy properties that evaluate to null (xabbuh) - * bug #37464 [ErrorHandler] fix throwing from __toString() (nicolas-grekas) - * bug #37449 [Translation] Fix caching of parent locales file in translator (jvasseur) - * bug #37418 [PhpUnitBridge] Fix compatibility with phpunit 9.3 (Gennadi Janzen) - * bug #37441 [DoctrineBridge] work around Connection::ping() deprecation (nicolas-grekas) - * bug #37291 [MimeType] Duplicated MimeType due to PHP Bug (juanmrad) - * bug #37429 [DI] fix parsing of argument type=binary in xml (Tobion) - * bug #37425 [Form] fix guessing form types for DateTime types (xabbuh) - * bug #37392 [Validator] fix handling typed properties as constraint options (xabbuh) - * bug #37358 Directly use the driverConnection executeUpdate method (TristanPouliquen) - * bug #37389 [HttpFondation] Change file extension of "audio/mpeg" from "mpga" to "mp3" (YaFou) - * bug #37379 [HttpClient] Support for cURL handler objects (derrabus) - * bug #37383 [VarDumper] Support for cURL handler objects (derrabus) - * bug #37395 add .body wrapper element (Nemo64) - * bug #37400 [HttpClient] unset activity list when creating CurlResponse (nicolas-grekas) - * bug #36304 Check whether path is file in DataPart::fromPath() (freiondrej) - * bug #37345 [Form] collect all transformation failures (xabbuh) - * bug #37362 [SecurityBundle] Drop cache.security_expression_language service if invalid (chalasr) - * bug #37353 [DI] disable preload.php on the CLI (nicolas-grekas) - * bug #37268 [Messenger] Fix precedence of DSN options for 4.4 (jderusse) - * bug #37341 Fix support for PHP8 union types (nicolas-grekas) - * bug #37271 [FrameworkBundle] preserve dots in query-string when redirecting (nicolas-grekas) - * bug #37340 Fix support for PHP8 union types (nicolas-grekas) - * bug #37275 [DI] tighten detection of local dirs to prevent false positives (nicolas-grekas) - * bug #37090 [PhpUnitBridge] Streamline ansi/no-ansi of composer according to phpunit --colors option (kick-the-bucket) - * bug #36230 [VarDumper] Fix CliDumper coloration on light arrays (l-vo) - * bug #37270 [FrameworkBundle] preserve dots in query-string when redirecting (nicolas-grekas) - * bug #37319 [HttpClient] Convert CurlHttpClient::handlePush() to instance method (mpesari) - * bug #37342 [Cache] fix compat with DBAL v3 (nicolas-grekas) - * bug #37286 [Console] Reset question validator attempts only for actual stdin (bis) (nicolas-grekas) - * bug #37160 Reset question validator attempts only for actual stdin (ostrolucky) - * bug #36975 [PropertyInfo] Make PhpDocExtractor compatible with phpDocumentor v5 (DerManoMann) - -* 4.4.10 (2020-06-12) - - * bug #37227 [DependencyInjection][CheckTypeDeclarationsPass] Handle unresolved parameters pointing to environment variables (fancyweb) - * bug #37103 [Form] switch the context when validating nested forms (xabbuh) - * bug #37182 [HttpKernel] Fix regression where Store does not return response body correctly (mpdude) - * bug #37193 [DependencyInjection][CheckTypeDeclarationsPass] Always resolve parameters (fancyweb) - * bug #37191 [HttpClient] fix offset computation for data chunks (nicolas-grekas) - * bug #37177 [Ldap] fix refreshUser() ignoring extra_fields (arkste) - * bug #37181 [Mailer] Remove an internal annot (fabpot) - * bug #36913 [FrameworkBundle] fix type annotation on ControllerTrait::addFlash() (ThomasLandauer) - * bug #37162 [Mailer] added the reply-to addresses to the API SES transport request. (ribeiropaulor) - * bug #37167 [Mime] use fromString when creating a new Address (fabpot) - * bug #37169 [Cache] fix forward compatibility with Doctrine DBAL 3 (xabbuh) - * bug #37159 [Mailer] Fixed generator bug when creating multiple transports using Transport::fromDsn (atailouloute) - * bug #37048 [HttpClient] fix monitoring timeouts when other streams are active (nicolas-grekas) - * bug #37085 [Form] properly cascade validation to child forms (xabbuh) - * bug #37095 [PhpUnitBridge] Fix undefined index when output of "composer show" cannot be parsed (nicolas-grekas) - * bug #37092 [PhpUnitBridge] fix undefined var on version 3.4 (nicolas-grekas) - * bug #37065 [HttpClient] Throw JsonException instead of TransportException on empty response in Response::toArray() (jeroennoten) - * bug #37077 [WebProfilerBundle] Move ajax clear event listener initialization on loadToolbar (Bruno BOUTAREL) - * bug #37049 [Serializer] take into account the context when preserving empty array objects (xabbuh) - -* 4.4.9 (2020-05-31) - - * bug #37008 [Security] Fixed AbstractToken::hasUserChanged() (wouterj) - * bug #36894 [Validator] never directly validate Existence (Required/Optional) constraints (xabbuh) - * bug #37007 [Console] Fix QuestionHelper::disableStty() (chalasr) - * bug #36865 [Form] validate subforms in all validation groups (xabbuh) - * bug #36907 Fixes sprintf(): Too few arguments in form transformer (pedrocasado) - * bug #36868 [Validator] Use Mime component to determine mime type for file validator (pierredup) - * bug #37000 Add meaningful message when using ProcessHelper and Process is not installed (l-vo) - * bug #36995 [TwigBridge] fix fallback html-to-txt body converter (nicolas-grekas) - * bug #36993 [ErrorHandler] fix setting $trace to null in FatalError (nicolas-grekas) - * bug #36987 Handle fetch mode deprecation of DBAL 2.11. (derrabus) - * bug #36974 [Security] Fixed handling of CSRF logout error (wouterj) - * bug #36947 [Mime] Allow email message to have "To", "Cc", or "Bcc" header to be valid (Ernest Hymel) - * bug #36914 Parse and render anonymous classes correctly on php 8 (derrabus) - * bug #36921 [OptionsResolver][Serializer] Remove calls to deprecated ReflectionParameter::getClass() (derrabus) - * bug #36920 [VarDumper] fix PHP 8 support (nicolas-grekas) - * bug #36917 [Cache] Accessing undefined constants raises an Error in php8 (derrabus) - * bug #36891 Address deprecation of ReflectionType::getClass() (derrabus) - * bug #36899 [VarDumper] ReflectionFunction::isDisabled() is deprecated (derrabus) - * bug #36905 [Validator] Catch expected ValueError (derrabus) - * bug #36915 [DomCrawler] Catch expected ValueError (derrabus) - * bug #36908 [Cache][HttpClient] Made method signatures compatible with their corresponding traits (derrabus) - * bug #36906 [DomCrawler] Catch expected ValueError (derrabus) - * bug #36904 [PropertyAccess] Parse php 8 TypeErrors correctly (derrabus) - * bug #36839 [BrowserKit] Raw body with custom Content-Type header (azhurb) - * bug #36896 [Config] Removed implicit cast of ReflectionProperty to string (derrabus) - * bug #35944 [Security/Core] Fix wrong roles comparison (thlbaut) - * bug #36882 [PhpUnitBridge] fix installing under PHP >= 8 (nicolas-grekas) - * bug #36833 [HttpKernel] Fix that the `Store` would not save responses with the X-Content-Digest header present (mpdude) - * bug #36867 [PhpUnitBridge] fix bad detection of unsilenced deprecations (nicolas-grekas) - * bug #36862 [Security] Unserialize $parentData, if needed, to avoid errors (rfaivre) - * bug #36855 [HttpKernel] Fix error logger when stderr is redirected to /dev/null (fabpot) - * bug #36838 [HttpKernel] Bring back the debug toolbar (derrabus) - * bug #36592 [BrowserKit] Allow Referer set by history to be overridden (Slamdunk) - * bug #36823 [HttpClient] fix PHP warning + accept status code >= 600 (nicolas-grekas) - * bug #36824 [Security/Core] fix compat of `NativePasswordEncoder` with pre-PHP74 values of `PASSWORD_*` consts (nicolas-grekas) - * bug #36811 [DependencyInjection] Fix register event listeners compiler pass (X-Coder264) - * bug #36789 Change priority of KernelEvents::RESPONSE subscriber (marcw) - * bug #36794 [Serializer] fix issue with PHP 8 (nicolas-grekas) - * bug #36786 [WebProfiler] Remove 'none' when appending CSP tokens (ndench) - * bug #36743 [Yaml] Fix escaped quotes in quoted multi-line string (ossinkine) - * bug #36777 [TwigBundle] FormExtension does not have a constructor anymore since sf 4.0 (Tobion) - * bug #36716 [Mime] handle passing custom mime types as string (mcneely) - * bug #36747 Queue name is a required parameter (theravel) - * bug #36751 [Mime] fix bad method call on `EmailAddressContains` (Kocal) - * bug #36696 [Console] don't check tty on stdin, it breaks with "data lost during stream conversion" (nicolas-grekas) - * bug #36569 [PhpUnitBridge] Mark parent class also covered in CoverageListener (lyrixx) - * bug #36690 [Yaml] prevent notice for invalid octal numbers on PHP 7.4 (xabbuh) - * bug #36590 [Console] Default hidden question to 1 attempt for non-tty session (ostrolucky) - * bug #36497 [Filesystem] Handle paths on different drives (crishoj) - * bug #36678 [WebProfiler] Do not add src-elem CSP directives if they do not exist (ndench) - * bug #36501 [DX] Show the ParseException message in all YAML file loaders (fancyweb) - * bug #36683 [Yaml] fix parse error when unindented collections contain a comment (wdiesveld) - * bug #36672 [Validator] Skip validation when email is an empty object (acrobat) - * bug #36673 [PhpUnitBridge] fix PHP 5.3 compat again (nicolas-grekas) - * bug #36505 [Translation] Fix for translation:update command updating ICU messages (artemoliynyk) - * bug #36627 [Validator] fix lazy property usage. (bendavies) - * bug #36601 [Serializer] do not transform empty \Traversable to Array (soyuka) - * bug #36606 [Cache] Fixed not supported Redis eviction policies (SerheyDolgushev) - * bug #36625 [PhpUnitBridge] fix compat with PHP 5.3 (nicolas-grekas) - -* 4.4.8 (2020-04-28) - - * bug #36536 [Cache] Allow invalidateTags calls to be traced by data collector (l-vo) - * bug #36566 [PhpUnitBridge] Use COMPOSER_BINARY env var if available (fancyweb) - * bug #36560 [YAML] escape DEL(\x7f) (sdkawata) - * bug #36539 [PhpUnitBridge] fix compatibility with phpunit 9 (garak) - * bug #36555 [Cache] skip APCu in chains when the backend is disabled (nicolas-grekas) - * bug #36523 [Form] apply automatically step=1 for datetime-local input (ottaviano) - * bug #36519 [FrameworkBundle] debug:autowiring: Fix wrong display when using class_alias (weaverryan) - * bug #36454 [DependencyInjection][ServiceSubscriber] Support late aliases (fancyweb) - * bug #36498 [Security/Core] fix escape for username in LdapBindAuthenticationProvider.php (stoccc) - * bug #36506 [FrameworkBundle] Fix session.attribute_bag service definition (fancyweb) - * bug #36500 [Routing][PrefixTrait] Add the _locale requirement (fancyweb) - * bug #36457 [Cache] CacheItem with tag is never a hit after expired (alexander-schranz, nicolas-grekas) - * bug #36490 [HttpFoundation] workaround PHP bug in the session module (nicolas-grekas) - * bug #36483 [SecurityBundle] fix accepting env vars in remember-me configurations (zek) - * bug #36343 [Form] Fixed handling groups sequence validation (HeahDude) - * bug #36460 [Cache] Avoid memory leak in TraceableAdapter::reset() (lyrixx) - * bug #36467 Mailer from sender fixes (fabpot) - * bug #36408 [PhpUnitBridge] add PolyfillTestCaseTrait::expectExceptionMessageMatches to provide FC with recent phpunit versions (soyuka) - * bug #36447 Remove return type for Twig function workflow_metadata() (gisostallenberg) - * bug #36449 [Messenger] Make sure redis transports are initialized correctly (Seldaek) - * bug #36411 [Form] RepeatedType should always have inner types mapped (biozshock) - * bug #36441 [DI] fix loading defaults when using the PHP-DSL (nicolas-grekas) - * bug #36434 [HttpKernel] silence E_NOTICE triggered since PHP 7.4 (xabbuh) - * bug #36365 [Validator] Fixed default group for nested composite constraints (HeahDude) - * bug #36422 [HttpClient] fix HTTP/2 support on non-SSL connections - CurlHttpClient only (nicolas-grekas) - * bug #36417 Force ping after transport exception (oesteve) - * bug #35591 [Validator] do not merge constraints within interfaces (greedyivan) - * bug #36377 [HttpClient] Fix scoped client without query option configuration (X-Coder264) - * bug #36387 [DI] fix detecting short service syntax in yaml (nicolas-grekas) - * bug #36392 [DI] add missing property declarations in InlineServiceConfigurator (nicolas-grekas) - * bug #36400 Allowing empty secrets to be set (weaverryan) - * bug #36380 [Process] Fixed input/output error on PHP 7.4 (mbardelmeijer) - * bug #36376 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx) - * bug #36375 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx) - * bug #36305 [PropertyInfo][ReflectionExtractor] Check the array mutator prefixes last when the property is singular (fancyweb) - * bug #35656 [HttpFoundation] Fixed session migration with custom cookie lifetime (Guite) - * bug #36342 [HttpKernel][FrameworkBundle] fix compat with Debug component (nicolas-grekas) - * bug #36315 [WebProfilerBundle] Support for Content Security Policy style-src-elem and script-src-elem in WebProfiler (ampaze) - * bug #36286 [Validator] Allow URL-encoded special characters in basic auth part of URLs (cweiske) - * bug #36335 [Security] Track session usage whenever a new token is set (wouterj) - * bug #36332 [Serializer] Fix unitialized properties (from PHP 7.4.2) when serializing context for the cache key (alanpoulain) - * bug #36337 [MonologBridge] Fix $level type (fancyweb) - * bug #36223 [Security][Http][SwitchUserListener] Ignore all non existent username protection errors (fancyweb) - * bug #36239 [HttpKernel][LoggerDataCollector] Prevent keys collisions in the sanitized logs processing (fancyweb) - * bug #36245 [Validator] Fixed calling getters before resolving groups (HeahDude) - * bug #36265 Fix the reporting of deprecations in twig:lint (stof) - * bug #36283 [Security] forward multiple attributes voting flag (xabbuh) - -* 4.4.7 (2020-03-30) - - * security #cve-2020-5255 [HttpFoundation] Do not set the default Content-Type based on the Accept header (yceruto) - * security #cve-2020-5275 [Security] Fix access_control behavior with unanimous decision strategy (chalasr) - * bug #36262 [DI] fix generating TypedReference from PriorityTaggedServiceTrait (nicolas-grekas) - * bug #36252 [Security/Http] Allow setting cookie security settings for delete_cookies (wouterj) - * bug #36261 [FrameworkBundle] revert to legacy wiring of the session when circular refs are detected (nicolas-grekas) - * bug #36259 [DomCrawler] Fix BC break in assertions breaking Panther (dunglas) - * bug #36181 [BrowserKit] fixed missing post request parameters in file uploads (codebay) - * bug #36216 [Validator] Assert Valid with many groups (phucwan91) - * bug #36222 [Console] Fix OutputStream for PHP 7.4 (guillbdx) - -* 4.4.6 (2020-03-27) - - * bug #36169 [HttpKernel] fix locking for PHP 7.4+ (nicolas-grekas) - * bug #36175 [Security/Http] Remember me: allow to set the samesite cookie flag (dunglas) - * bug #36173 [Http Foundation] Fix clear cookie samesite (guillbdx) - * bug #36176 [Security] Check if firewall is stateless before checking for session/previous session (koenreiniers) - * bug #36149 [Form] Support customized intl php.ini settings (jorrit) - * bug #36172 [Debug] fix for PHP 7.3.16+/7.4.4+ (nicolas-grekas) - * bug #36151 [Security] Fixed hardcoded value of SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE (lyrixx) - * bug #36141 Prevent warning in proc_open() (BenMorel) - * bug #36143 [FrameworkBundle] Fix Router Cache (guillbdx) - * bug #36103 [DI] fix preloading script generation (nicolas-grekas) - * bug #36118 [Security/Http] don't require the session to be started when tracking its id (nicolas-grekas) - * bug #36108 [DI] Fix CheckTypeDeclarationPass (guillbdx) - * bug #36121 [VarDumper] fix side-effect by not using mt_rand() (nicolas-grekas) - * bug #36073 [PropertyAccess][DX] Improved errors when reading uninitialized properties (HeahDude) - * bug #36063 [FrameworkBundle] start session on flashbag injection (William Arslett) - * bug #36031 [Console] Fallback to default answers when unable to read input (ostrolucky) - * bug #36083 [DI][Form] Fixed test suite (TimeType changes & unresolved merge conflict) (wouterj) - * bug #36026 [Mime] Fix boundary header (guillbdx) - * bug #36020 [Form] ignore microseconds submitted by Edge (xabbuh) - * bug #36038 [HttpClient] disable debug log with curl 7.64.0 (nicolas-grekas) - * bug #36041 fix import from config file using type: glob (Tobion) - * bug #35987 [DoctrineBridge][DoctrineExtractor] Fix wrong guessed type for "json" type (fancyweb) - * bug #35949 [DI] Fix container lint command when a synthetic service is used in an expression (HypeMC) - * bug #36023 [HttpClient] fix requests to hosts that idn_to_ascii() cannot handle (nicolas-grekas) - * bug #35938 [Form] Handle false as empty value on expanded choices (fancyweb) - * bug #36030 [SecurityBundle] Minor fix in LDAP config tree builder (HeahDude) - * bug #35993 Remove int return type from FlattenException::getCode (wucdbm) - * bug #36004 [Yaml] fix dumping strings containing CRs (xabbuh) - * bug #35982 [DI] Fix XmlFileLoader bad error message (przemyslaw-bogusz) - * bug #35957 [DI] ignore extra tags added by autoconfiguration in PriorityTaggedServiceTrait (nicolas-grekas) - * bug #35937 Revert "bug symfony#28179 [DomCrawler] Skip disabled fields processing in Form" (dmaicher) - * bug #35928 [Routing] Prevent localized routes _locale default & requirement from being overridden (fancyweb) - * bug #35912 [FrameworkBundle] register only existing transport factories (xabbuh) - * bug #35899 [DomCrawler] prevent deprecation being triggered from assertion (xabbuh) - * bug #35910 [SecurityBundle] Minor fixes in configuration tree builder (HeahDude) - -* 4.4.5 (2020-02-29) - - * bug #35781 [Form] NumberToLocalizedStringTransformer return int if scale = 0 (VincentLanglet) - * bug #35846 [Serializer] prevent method calls on null values (xabbuh) - * bug #35897 [FrameworkBundle] add missing Messenger options to XML schema definition (xabbuh) - * bug #35870 [ErrorHandler] fix parsing static return type on interface method annotation (alekitto) - * bug #35839 [Security] Allow switching to another user when already switched (chalasr) - * bug #35851 [DoctrineBridge] Use new Types::* constants and support new json types (fancyweb) - * bug #35716 [PhpUnitBridge] Fix compatibility to PHPUnit 9 (Benjamin) - * bug #35803 [Cache] Fix versioned namespace atomic clears (trvrnrth) - * bug #35817 [DoctrineBridge] Use new Types::* constants and support new json type (fancyweb) - * bug #35832 [Debug][ErrorHandler] improved deprecation notices for methods new args and return type (HeahDude) - * bug #35827 [BrowserKit] Nested file array prevents uploading file (afilina) - * bug #35707 [ExpressionLanguage] Fixed collisions of character operators with object properties (Andrej-in-ua) - * bug #35794 [DoctrineBridge][DoctrineExtractor] Fix indexBy with custom and some core types (fancyweb) - * bug #35787 [PhpUnitBridge] Use trait instead of extending deprecated class (marcello-moenkemeyer) - * bug #35792 [Security] Prevent TypeError in case RememberMetoken has no attached user (nikophil) - * bug #35735 [Routing] Add locale requirement for localized routes (mtarld) - * bug #35772 [Config] don't throw on missing excluded paths (nicolas-grekas) - * bug #35774 [Ldap] force default network timeout (nicolas-grekas) - * bug #35702 [VarDumper] fixed DateCaster not displaying additional fields (Makdessi Alex) - * bug #35722 [HttpKernel] Set previous exception when rethrown from controller resolver (danut007ro) - * bug #35714 [HttpClient] Correctly remove trace level options for HttpCache (aschempp) - * bug #35718 [HttpKernel] fix registering DebugHandlersListener regardless of the PHP_SAPI (nicolas-grekas) - * bug #35728 Add missing autoload calls (greg0ire) - * bug #35693 [Finder] Fix unix root dir issue (chr-hertel) - * bug #35709 [HttpFoundation] fix not sending Content-Type header for 204 responses (Tobion) - * bug #35710 [ErrorHandler] silence warning when zend.assertions=-1 (nicolas-grekas) - * bug #35676 [Console] Handle zero row count in appendRow() for Table (Adam Prickett) - * bug #35696 [Console] Don't load same-namespace alternatives on exact match (chalasr) - * bug #35674 [HttpClient] fix getting response content after its destructor throwed an HttpExceptionInterface (nicolas-grekas) - * bug #35672 [HttpClient] fix HttpClientDataCollector when handling canceled responses (thematchless) - * bug #35641 [Process] throw when PhpProcess::fromShellCommandLine() is used (Guikingone) - * bug #35645 [ErrorHandler] Never throw on warnings triggered by assert() and set assert.exception=1 in Debug::enable() (nicolas-grekas) - * bug #35633 [Mailer] Do not ping the SMTP server before sending every message (micheh) - * bug #33897 [Console] Consider STDIN interactive (ostrolucky) - * bug #35605 [HttpFoundation][FrameworkBundle] fix support for samesite in session cookies (fabpot) - * bug #35609 [DoctrineBridge] Fixed submitting ids with query limit or offset (HeahDude) - * bug #35597 [PHPunit bridge] Provide current file as file path (greg0ire) - * bug #33960 [DI] Unknown env prefix not recognized as such (ro0NL) - * bug #35342 [DI] Fix support for multiple tags for locators and iterators (Alexandre Parent) - * bug #33820 [PhpUnitBridge] Fix some errors when using serialized deprecations (l-vo) - * bug #35553 Fix HTTP client config handling (julienfalque) - * bug #35588 [ErrorHandler] Escape variable in Exception template (jderusse) - * bug #35583 Add missing use statements (fabpot) - * bug #35582 Missing use statement 4.4 (fabpot) - * bug #34123 [Form] Fix handling of empty_data's \Closure value in Date/Time form types (yceruto) - * bug #35537 [Config][XmlReferenceDumper] Prevent potential \TypeError (fancyweb) - * bug #35227 [Mailer] Fix broken mandrill http send for recipients with names (vilius-g) - * bug #35430 [Translation] prefer intl domain when adding messages to catalogue (Guite) - * bug #35497 Fail on empty password verification (without warning on any implementation) (Stefan Kruppa) - * bug #35546 [Validator] check for __get method existence if property is uninitialized (alekitto) - * bug #35332 [Yaml][Inline] Fail properly on empty object tag and empty const tag (fancyweb) - * bug #35489 [PhpUnitBridge] Fix running skipped tests expecting only deprecations (chalasr) - * bug #35161 [FrameworkBundle] Check non-null type for numeric type (Arman-Hosseini) - * bug #34059 [DomCrawler] Skip disabled fields processing in Form (sbogx) - * bug #34114 [Console] SymonfyStyle - Check value isset to avoid PHP notice (leevigraham) - * bug #35557 [Config] dont catch instances of Error (nicolas-grekas) - * bug #35562 [HttpClient] fix HttpClientDataCollector when handling canceled responses (nicolas-grekas) - -* 4.4.4 (2020-01-31) - - * bug #35530 [HttpClient] Fix regex bearer (noniagriconomie) - * bug #35532 [Validator] fix access to uninitialized property when getting value (greedyivan) - * bug #35486 [Translator] Default value for 'sort' option in translation:update should be 'asc' (versgui) - * bug #35305 [HttpKernel] Fix stale-if-error behavior, add tests (mpdude) - * bug #34808 [PhpUnitBridge] Properly handle phpunit arguments for configuration file (biozshock) - * bug #35517 [Intl] Provide more locale translations (ro0NL) - * bug #35518 [Mailer] Fix STARTTLS support for Postmark and Mandrill (fabpot) - * bug #35480 [Messenger] Check for all serialization exceptions during message dec… (Patrick Berenschot) - * bug #35502 [Messenger] Fix bug when using single route with XML config (Nyholm) - * bug #35438 [SecurityBundle] fix ldap_bind service arguments (Ioni14) - * bug #35429 [DI] CheckTypeDeclarationsPass now checks if value is type of parameter type (pfazzi) - * bug #35464 [ErrorHandler] Add debug argument to decide whether debug page is shown or not (yceruto) - * bug #35423 Fixes a runtime error when accessing the cache panel (DamienHarper) - * bug #35428 [Cache] fix checking for igbinary availability (nicolas-grekas) - * bug #35424 [HttpKernel] Check if lock can be released (sjadema) - -* 4.4.3 (2020-01-21) - - * bug #35364 [Yaml] Throw on unquoted exclamation mark (fancyweb) - * bug #35065 [Security] Use supportsClass in addition to UnsupportedUserException (linaori) - * bug #35351 Revert #34797 "Fixed translations file dumper behavior" and fix #34713 (yceruto) - * bug #35355 [DI] Fix EnvVar not loaded when Loader requires an env var (jderusse) - * bug #35343 [Security] Fix RememberMe with null password (jderusse) - * bug #34223 [DI] Suggest typed argument when binding fails with untyped argument (gudfar) - * bug #35323 [FrameworkBundle] Set booted flag to false when test kernel is unset (thiagocordeiro) - * bug #35324 [HttpClient] Fix strict parsing of response status codes (Armando-Walmeric) - * bug #35318 [Yaml] fix PHP const mapping keys using the inline notation (xabbuh) - * bug #35306 [FrameworkBundle] Make sure one can use fragments.hinclude_default_template (Nyholm) - * bug #35304 [HttpKernel] Fix that no-cache MUST revalidate with the origin (mpdude) - * bug #35299 Avoid `stale-if-error` in FrameworkBundle's HttpCache if kernel.debug = true (mpdude) - * bug #35240 [SecurityBundle] Fix collecting traceable listeners info on lazy firewalls (chalasr) - * bug #35151 [DI] deferred exceptions in ResolveParameterPlaceHoldersPass (Islam93) - * bug #35290 [Filesystem][FilesystemCommonTrait] Use a dedicated directory when there are no namespace (fancyweb) - * bug #35099 [FrameworkBundle] Do not throw exception on value generate key (jderusse) - * bug #35278 [EventDispatcher] expand listener in place (xabbuh) - * bug #35269 [HttpKernel][FileLocator] Fix deprecation message (fancyweb) - * bug #35254 [PHPUnit-Bridge] Fail-fast in simple-phpunit if one of the passthru() commands fails (mpdude) - * bug #35261 [Routing] Fix using a custom matcher & generator dumper class (fancyweb) - * bug #34643 [Dotenv] Fixed infinite loop with missing quote followed by quoted value (naitsirch) - * bug #35239 [Security\Http] Prevent canceled remember-me cookie from being accepted (chalasr) - * bug #35267 [Debug] fix ClassNotFoundFatalErrorHandler (nicolas-grekas) - * bug #35252 [Serializer] Fix cache in MetadataAwareNameConverter (bastnic) - * bug #35200 [TwigBridge] do not render preferred choices as selected (xabbuh) - * bug #35243 [HttpKernel] release lock explicitly (nicolas-grekas) - * bug #35193 [TwigBridge] button_widget now has its title attr translated even if its label = null or false (stephen-lewis) - * bug #35219 [PhpUnitBridge] When using phpenv + phpenv-composer plugin, composer executable is wrapped into a bash script (oleg-andreyev) - * bug #35150 [Messenger] Added check if json_encode succeeded (toooni) - * bug #35137 [Messenger] Added check if json_encode succeeded (toooni) - * bug #35170 [FrameworkBundle][TranslationUpdateCommand] Do not output positive feedback on stderr (fancyweb) - * bug #35245 [HttpClient] fix exception in case of PSR17 discovery failure (nicolas-grekas) - * bug #35244 [Cache] fix processing chain adapter based cache pool (xabbuh) - * bug #35247 [FrameworkBundle][ContainerLintCommand] Only skip .errored. services (fancyweb) - * bug #35225 [DependencyInjection] Handle ServiceClosureArgument for callable in container linting (shieldo) - * bug #35223 [HttpClient] Don't read from the network faster than the CPU can deal with (nicolas-grekas) - * bug #35214 [DI] DecoratorServicePass should keep container.service_locator on the decorated definition (malarzm) - * bug #35209 [HttpClient] fix support for non-blocking resource streams (nicolas-grekas) - * bug #35210 [HttpClient] NativeHttpClient should not send >1.1 protocol version (nicolas-grekas) - * bug #35162 [Mailer] Make sure you can pass custom headers to Mailgun (Nyholm) - * bug #33672 [Mailer] Remove line breaks in email attachment content (Stuart Fyfe) - * bug #35101 [Routing] Fix i18n routing when the url contains the locale (fancyweb) - * bug #35124 [TwigBridge][Form] Added missing help messages in form themes (cmen) - * bug #35195 [HttpClient] fix casting responses to PHP streams (nicolas-grekas) - * bug #35168 [HttpClient] fix capturing SSL certificates with NativeHttpClient (nicolas-grekas) - * bug #35134 [PropertyInfo] Fix BC issue in phpDoc Reflection library (jaapio) - * bug #35184 [Mailer] Payload sent to Sendgrid doesn't include names (versgui) - * bug #35173 [Mailer][MailchimpBridge] Fix missing attachments when sending via Mandrill API (vilius-g) - * bug #35172 [Mailer][MailchimpBridge] Fix incorrect sender address when sender has name (vilius-g) - * bug #35125 [Translator] fix performance issue in MessageCatalogue and catalogue operations (ArtemBrovko) - * bug #35120 [HttpClient] fix scheduling pending NativeResponse (nicolas-grekas) - * bug #35117 [Cache] do not overwrite variable value (xabbuh) - * bug #35113 [VarDumper] Fix "Undefined index: argv" when using CliContextProvider (xepozz) - * bug #34673 Migrate server:log command away from WebServerBundle (jderusse) - * bug #35103 [Translation] Use `locale_parse` for computing fallback locales (alanpoulain) - * bug #35060 [Security] Fix missing defaults for auto-migrating encoders (chalasr) - * bug #35067 [DependencyInjection][CheckTypeDeclarationsPass] Handle \Closure for callable (fancyweb) - * bug #35094 [Console] Fix filtering out identical alternatives when there is a command loader (fancyweb) - -* 4.4.2 (2019-12-19) - - * bug #35051 [DependencyInjection] Fix binding tagged services to containers (nicolas-grekas) - * bug #35039 [DI] skip looking for config class when the extension class is anonymous (nicolas-grekas) - * bug #35049 [ProxyManager] fix generating proxies for root-namespaced classes (nicolas-grekas) - * bug #35022 [Dotenv] FIX missing getenv (mccullagh) - * bug #35023 [HttpKernel] ignore failures generated by opcache.restrict_api (nicolas-grekas) - * bug #35024 [HttpFoundation] fix pdo session handler for sqlsrv (azjezz) - * bug #35025 [HttpClient][Psr18Client] Remove Psr18ExceptionTrait (fancyweb) - * bug #35015 [Config] fix perf of glob discovery when GLOB_BRACE is not available (nicolas-grekas) - * bug #35014 [HttpClient] make pushed responses retry-able (nicolas-grekas) - * bug #35010 [VarDumper] ignore failing __debugInfo() (nicolas-grekas) - * bug #34998 [DI] fix auto-binding service providers to their service subscribers (nicolas-grekas) - * bug #34954 [Mailer] Fixed undefined index when sending via Mandrill API (wulff) - * bug #33670 [DI] Service locators can't be decorated (malarzm) - * bug #35000 [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt (fancyweb) - * bug #35005 [HttpClient] force HTTP/1.1 when NTLM auth is used (nicolas-grekas) - * bug #34707 [Validation][FrameworkBundle] Allow EnableAutoMapping to work without auto-mapping namespaces (ogizanagi) - * bug #34996 Fix displaying anonymous classes on PHP 7.4 (nicolas-grekas) - * bug #29839 [Validator] fix comparisons with null values at property paths (xabbuh) - * bug #34900 [DoctrineBridge] Fixed submitting invalid ids when using queries with limit (HeahDude) - * bug #34791 [Serializer] Skip uninitialized (PHP 7.4) properties in PropertyNormalizer and ObjectNormalizer (vudaltsov) - * bug #34956 [Messenger][AMQP] Use delivery_mode=2 by default (lyrixx) - * bug #34915 [FrameworkBundle] Fix invalid Windows path normalization in TemplateNameParser (mvorisek) - * bug #34981 stop using deprecated Doctrine persistence classes (xabbuh) - * bug #34904 [Validator][ConstraintValidator] Safe fail on invalid timezones (fancyweb) - * bug #34935 [FrameworkBundle][DependencyInjection] Skip removed ids in the lint container command and its associated pass (fancyweb) - * bug #34957 [Security] Revert "AbstractAuthenticationListener.php error instead info" (larzuk91) - * bug #34922 [FrameworkBundle][Secrets] Hook configured local dotenv file (fancyweb) - * bug #34967 [HttpFoundation] fix redis multi host dsn not recognized (Jan Christoph Beyer) - * bug #34963 [Lock] fix constructor argument type declaration (xabbuh) - * bug #34955 Require doctrine/persistence ^1.3 (nicolas-grekas) - * bug #34923 [DI] Fix support for immutable setters in CallTrait (Lctrs) - * bug #34878 [TwigBundle] fix broken FilesystemLoader::exists() with Twig 3 (dpesch) - * bug #34921 [HttpFoundation] Removed "Content-Type" from the preferred format guessing mechanism (yceruto) - * bug #34886 [HttpKernel] fix triggering deprecation in file locator (xabbuh) - * bug #34918 [Translation] fix memoryleak in PhpFileLoader (nicolas-grekas) - * bug #34920 [Routing] fix memoryleak when loading compiled routes (nicolas-grekas) - * bug #34787 [Cache] Propagate expiry when syncing items in ChainAdapter (trvrnrth) - * bug #34694 [Validator] Fix auto-mapping constraints should not be validated (ogizanagi) - * bug #34848 [Process] change the syntax of portable command lines (nicolas-grekas) - * bug #34862 [FrameworkBundle][ContainerLintCommand] Reinitialize bundles when the container is reprepared (fancyweb) - * bug #34896 [Cache] fix memory leak when using PhpFilesAdapter (nicolas-grekas) - * bug #34438 [HttpFoundation] Use `Cache-Control: must-revalidate` only if explicit lifetime has been given (mpdude) - * bug #34449 [Yaml] Implement multiline string as scalar block for tagged values (natepage) - * bug #34601 [MonologBridge] Fix debug processor datetime type (mRoca) - * bug #34842 [ExpressionLanguage] Process division by zero (tigr1991) - * bug #34902 [PropertyAccess] forward caught exception (xabbuh) - * bug #34903 Fixing bad order of operations with null coalescing operator (weaverryan) - * bug #34888 [TwigBundle] add tags before processing them (xabbuh) - * bug #34760 [Mailer] Fix SMTP Authentication when using STARTTLS (DjLeChuck) - * bug #34762 [Config] never try loading failed classes twice with ClassExistenceResource (nicolas-grekas) - * bug #34783 [DependencyInjection] Handle env var placeholders in CheckTypeDeclarationsPass (fancyweb) - * bug #34839 [Cache] fix memory leak when using PhpArrayAdapter (nicolas-grekas) - * bug #34812 [Yaml] fix parsing negative octal numbers (xabbuh) - * bug #34854 [Messenger] gracefully handle missing event dispatchers (xabbuh) - * bug #34802 [Security] Check UserInterface::getPassword is not null before calling needsRehash (dbrekelmans) - * bug #34788 [SecurityBundle] Properly escape regex in AddSessionDomainConstraintPass (fancyweb) - * bug #34859 [SecurityBundle] Fix TokenStorage::reset not called in stateless firewall (jderusse) - * bug #34827 [HttpFoundation] get currently session.gc_maxlifetime if ttl doesnt exists (rafaeltovar) - * bug #34755 [FrameworkBundle] resolve service locators in `debug:*` commands (nicolas-grekas) - * bug #34832 [Validator] Allow underscore character "_" in URL username and password (romainneutron) - * bug #34811 [TwigBridge] Update bootstrap_4_layout.html.twig missing switch-custom label (sabruss) - * bug #34820 [FrameworkBundle][SodiumVault] Create secrets directory only when it is used (fancyweb) - * bug #34776 [DI] fix resolving bindings for named TypedReference (nicolas-grekas) - * bug #34794 [DependencyInjection] Resolve expressions in CheckTypeDeclarationsPass (fancyweb) - * bug #34797 [Translation] Fix FileDumper behavior (yceruto) - * bug #34738 [SecurityBundle] Passwords are not encoded when algorithm set to "true" (nieuwenhuisen) - * bug #34759 [SecurityBundle] Fix switch_user provider configuration handling (fancyweb) - * bug #34779 [Security] do not validate passwords when the hash is null (xabbuh) - * bug #34786 [SecurityBundle] Use config variable in AnonymousFactory (martijnboers) - * bug #34784 [FrameworkBundle] Set the parameter bag as resolved in ContainerLintCommand (fancyweb) - * bug #34763 [Security/Core] Fix checking for SHA256/SHA512 passwords (David Brooks) - * bug #34757 [DI] Fix making the container path-independent when the app is in /app (nicolas-grekas) - -* 4.4.1 (2019-12-01) - - * bug #34732 [DependencyInjection][Xml] Fix the attribute 'tag' is not allowed in 'bind' tag (tienvx) - * bug #34729 [DI] auto-register singly implemented interfaces by default (nicolas-grekas) - * bug #34728 [DI] fix overriding existing services with aliases for singly-implemented interfaces (nicolas-grekas) - * bug #34649 more robust initialization from request (dbu) - * bug #34715 [TwigBundle] remove service when base class is missing (xabbuh) - * bug #34600 [DoctrineBridge] do not depend on the QueryBuilder from the ORM (xabbuh) - * bug #34627 [Security/Http] call auth listeners/guards eagerly when they "support" the request (nicolas-grekas) - * bug #34671 [Security] Fix clearing remember-me cookie after deauthentication (chalasr) - * bug #34711 Fix the translation commands when a template contains a syntax error (fabpot) - * bug #34032 [Mime] Fixing multidimensional array structure with FormDataPart (jvahldick) - * bug #34560 [Config][ReflectionClassResource] Handle parameters with undefined constant as their default values (fancyweb) - * bug #34695 [Config] don't break on virtual stack frames in ClassExistenceResource (nicolas-grekas) - * bug #34716 [DependencyInjection] fix dumping number-like string parameters (xabbuh) - * bug #34558 [Console] Fix autocomplete multibyte input support (fancyweb) - * bug #34130 [Console] Fix commands description with numeric namespaces (fancyweb) - * bug #34562 [DI] Skip unknown method calls for factories in check types pass (fancyweb) - * bug #34677 [EventDispatcher] Better error reporting when arguments to dispatch() are swapped (rimas-kudelis) - * bug #33573 [TwigBridge] Add row_attr to all form themes (fancyweb) - * bug #34019 [Serializer] CsvEncoder::NO_HEADERS_KEY ignored when used in constructor (Dario Savella) - * bug #34083 [Form] Keep preferred_choices order for choice groups (vilius-g) - * bug #34091 [Debug] work around failing chdir() on Darwin (mary2501) - * bug #34305 [PhpUnitBridge] Read configuration CLI directive (ro0NL) - * bug #34490 [Serializer] Fix MetadataAwareNameConverter usage with string group (antograssiot) - * bug #34632 [Console] Fix trying to access array offset on value of type int (Tavafi) - * bug #34669 [HttpClient] turn exception into log when the request has no content-type (nicolas-grekas) - * bug #34662 [HttpKernel] Support typehint to deprecated FlattenException in controller (andrew-demb) - * bug #34619 Restores preview mode support for Html and Serializer error renderers (yceruto) - * bug #34636 [VarDumper] notice on potential undefined index (sylvainmetayer) - * bug #34668 [Cache] Make sure we get the correct number of values from redis::mget() (thePanz) - * bug #34621 [Routing] Continue supporting single colon in object route loaders (fancyweb) - * bug #34554 [HttpClient] Fix early cleanup of pushed HTTP/2 responses (lyrixx) - * bug #34607 [HttpKernel] Ability to define multiple kernel.reset tags (rmikalkenas) - * bug #34599 [Mailer][Mailchimp Bridge] Throwing undefined index _id when setting message id (monteiro) - * bug #34569 [Workflow] Apply the same logic of precedence between the apply() and the buildTransitionBlockerList() method (lyrixx) - * bug #34580 [HttpKernel] Don't cache "not-fresh" state (nicolas-grekas) - * bug #34577 [FrameworkBundle][Cache] Don't deep-merge cache pools configuration (alxndrbauer) - * bug #34515 [DependencyInjection] definitions are valid objects (xabbuh) - * bug #34536 [SecurityBundle] Don't require a user provider for the anonymous listener (chalasr) - * bug #34533 [Monolog Bridge] Fixed accessing static property as non static. (Sander-Toonen) - * bug #34502 [FrameworkBundle][ContainerLint] Keep "removing" compiler passes (fancyweb) - * bug #34552 [Dotenv] don't fail when referenced env var does not exist (xabbuh) - * bug #34546 [Serializer] Add DateTimeZoneNormalizer into Dependency Injection (jewome62) - * bug #34547 [Messenger] Error when specified default bus is not among the configured (vudaltsov) - * bug #34513 [Validator] remove return type declaration from __sleep() (xabbuh) - * bug #34551 [Security] SwitchUser is broken when the User Provider always returns a valid user (tucksaun) - * bug #34385 Avoid empty "If-Modified-Since" header in validation request (mpdude) - * bug #34458 [Validator] ConstraintValidatorTestCase: add missing return value to mocked validate method calls (ogizanagi) - * bug #34516 [HttpKernel] drop return type declaration (xabbuh) - * bug #34474 [Messenger] Ignore stamps in in-memory transport (tienvx) - -* 4.4.0 (2019-11-21) - - * bug #34464 [Form] group constraints when calling the validator (nicolas-grekas) - * bug #34451 [DependencyInjection] Fix dumping multiple deprecated aliases (shyim) - * bug #34448 [Form] allow button names to start with uppercase letter (xabbuh) - * bug #34428 [Security] Fix best encoder not wired using migrate_from (chalasr) - -* 4.4.0-RC1 (2019-11-17) - - * bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas) - * bug #34347 [Messenger] Perform no deep merging of bus middleware (vudaltsov) - * bug #34366 [HttpFoundation] Allow redirecting to URLs that contain a semicolon (JayBizzle) - * feature #34405 [HttpFoundation] Added possibility to configure expiration time in redis session handler (mantulo) - * bug #34397 [FrameworkBundle] Remove project dir from Translator cache vary scanned directories (fancyweb) - * bug #34384 [DoctrineBridge] Improve queries parameters display in Profiler (fancyweb) - * bug #34408 [Cache] catch exceptions when using PDO directly (xabbuh) - * bug #34411 [HttpKernel] Flatten "exception" controller argument if not typed (chalasr) - * bug #34410 [HttpFoundation] Fix MySQL column type definition. (jbroutier) - * bug #34403 [Cache] Redis Tag Aware warn on wrong eviction policy (andrerom) - * bug #34400 [HttpKernel] collect bundle classes, not paths (nicolas-grekas) - * bug #34398 [Config] fix id-generation for GlobResource (nicolas-grekas) - * bug #34404 [HttpClient] fix HttpClientDataCollector (nicolas-grekas) - * bug #34396 [Finder] Allow ssh2 stream wrapper for sftp (damienalexandre) - * bug #34383 [DI] Use reproducible entropy to generate env placeholders (nicolas-grekas) - * bug #34389 [WebProfilerBundle] add FrameworkBundle requirement (xabbuh) - * bug #34381 [WebProfilerBundle] Require symfony/twig-bundle (fancyweb) - * bug #34358 [Security] always check the token on non-lazy firewalls (nicolas-grekas, lyrixx) - * bug #34390 [FrameworkBundle] fix wiring of httplug client (nicolas-grekas) - * bug #34369 [FrameworkBundle] Disallow WebProfilerBundle < 4.4 (derrabus) - * bug #34370 [DI] fix detecting singly implemented interfaces (nicolas-grekas) - -* 4.4.0-BETA2 (2019-11-13) - - * bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas) - * security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas) - * security #cve-2019-18888 [Mime] fix guessing mime-types of files with leading dash (nicolas-grekas) - * security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas) - * security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas) - * security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas) - * security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof) - -* 4.4.0-BETA1 (2019-11-12) - - * feature #34333 Revert "feature #34329 [ExpressionLanguage] add XOR operator (ottaviano)" (nicolas-grekas) - * feature #34332 Allow \Throwable $previous everywhere (fancyweb) - * feature #34329 [ExpressionLanguage] add XOR operator (ottaviano) - * feature #34312 [ErrorHandler] merge and remove the ErrorRenderer component (nicolas-grekas, yceruto) - * feature #34309 [HttpKernel] make ExceptionEvent able to propagate any throwable (nicolas-grekas) - * feature #34139 [Security] Add migrating encoder configuration (chalasr) - * feature #32194 [HttpFoundation] Add a way to anonymize IPs (Seldaek) - * feature #34252 [Console] Add support for NO_COLOR env var (Seldaek) - * feature #34295 [DI][FrameworkBundle] add EnvVarLoaderInterface - remove SecretEnvVarProcessor (nicolas-grekas) - * feature #31310 [DependencyInjection] Added option `ignore_errors: not_found` for imported config files (pulzarraider) - * feature #34216 [HttpClient] allow arbitrary JSON values in requests (pschultz) - * feature #31977 Add handling for delayed message to redis transport (alexander-schranz) - * feature #34217 [Messenger] use events consistently in worker (Tobion) - * feature #33065 Deprecate things that prevent \Throwable from bubbling down (fancyweb) - * feature #34184 [VarDumper] display the method we're in when dumping stack traces (nicolas-grekas) - * feature #33732 [Console] Rename some methods related to redraw frequency (javiereguiluz) - * feature #31587 [Routing][Config] Allow patterns of resources to be excluded from config loading (tristanbes) - * feature #32256 [DI] Add compiler pass and command to check that services wiring matches type declarations (alcalyn, GuilhemN, nicolas-grekas) - * feature #32061 Add new Form WeekType (dFayet) - * feature #33954 Form theme: support Bootstrap 4 custom switches (romaricdrigon) - * feature #33854 [DI] Add ability to choose behavior of decorations on non existent decorated services (mtarld) - * feature #34185 [Messenger] extract worker logic to listener and get rid of SendersLocatorInterface::getSenderByAlias (Tobion) - * feature #34156 Adding DoctrineClearEntityManagerWorkerSubscriber to reset EM in worker (weaverryan) - * feature #34133 [Cache] add DeflateMarshaller - remove phpredis compression (nicolas-grekas) - * feature #34177 [HttpFoundation][FrameworkBundle] allow configuring the session handler with a DSN (nicolas-grekas) - * feature #32107 [Validator] Add AutoMapping constraint to enable or disable auto-validation (dunglas) - * feature #34170 Re-allow to use "tagged" in service definitions (dunglas) - * feature #34043 [Lock] Add missing lock connection string in FrameworkExtension (jderusse) - * feature #34057 [Lock][Cache] Allows URL DSN in PDO adapters (jderusse) - * feature #34151 [DomCrawler] normalizeWhitespace should be true by default (dunglas) - * feature #34020 [Security] Allow to stick to a specific password hashing algorithm (chalasr) - * feature #34131 [FrameworkBundle] Remove suffix convention when using env vars to override secrets from the vault (nicolas-grekas) - * feature #34051 [HttpClient] allow option "buffer" to be a stream resource (nicolas-grekas) - * feature #34028 [ExpressionLanguage][Lexer] Exponential format for number (tigr1991) - * feature #34069 [Messenger] Removing "sync" transport and replacing it with config trick (weaverryan) - * feature #34014 [DI] made the `env(base64:...)` processor able to decode base64url (nicolas-grekas) - * feature #34044 [HttpClient] Add a canceled state to the ResponseInterface (Toflar) - * feature #33997 [FrameworkBundle] Add `secrets:*` commands and `env(secret:...)` processor to deal with secrets seamlessly (Tobion, jderusse, nicolas-grekas) - * feature #34013 [DI] add `LazyString` for lazy computation of string values injected into services (nicolas-grekas) - * feature #33961 [TwigBridge] Add show-deprecations option to the lint:twig command (yceruto) - * feature #33973 [HttpClient] add HttpClient::createForBaseUri() (nicolas-grekas) - * feature #33980 [HttpClient] try using php-http/discovery when nyholm/psr7 is not installed (nicolas-grekas) - * feature #33967 [Mailer] Add Message-Id to SentMessage when sending an email (fabpot) - * feature #33896 [Serializer][CSV] Add context options to handle BOM (malarzm) - * feature #33883 [Mailer] added ReplyTo option for PostmarkApiTransport (pierregaste) - * feature #33053 [ErrorHandler] Rework fatal errors (fancyweb) - * feature #33939 [Cache] add TagAwareMarshaller to optimize data storage when using AbstractTagAwareAdapter (nicolas-grekas) - * feature #33941 Keeping backward compatibility with legacy FlattenException usage (yceruto) - * feature #33851 [EventDispatcher] Allow to omit the event name when registering listeners (derrabus) - * feature #33461 [Cache] Improve RedisTagAwareAdapter invalidation logic & requirements (andrerom) - * feature #33779 [DI] enable improved syntax for defining method calls in Yaml (nicolas-grekas) - * feature #33743 [HttpClient] Async HTTPlug client (Nyholm) - * feature #33856 [Messenger] Allow to configure the db index on Redis transport (chalasr) - * feature #33881 [VarDumper] Added a support for casting Ramsey/Uuid (lyrixx) - * feature #33861 [CssSelector] Support *:only-of-type (jakzal) - * feature #33793 [EventDispatcher] A compiler pass for aliased userland events (derrabus) - * feature #33791 [Form] Added CountryType option for using alpha3 country codes (creiner) - * feature #33628 [DependencyInjection] added Ability to define a priority method for tagged service (lyrixx) - * feature #33775 [Console] Add deprecation message for non-int statusCode (jschaedl) - * feature #33783 [WebProfilerBundle] Try to display the most useful panel by default (fancyweb) - * feature #33701 [HttpKernel] wrap compilation of the container in an opportunistic lock (nicolas-grekas) - * feature #33789 [Serializer] Deprecate the XmlEncoder::TYPE_CASE_ATTRIBUTES constant (pierredup) - * feature #33776 Copy phpunit.xsd to a predictable path (julienfalque) - * feature #31446 [VarDumper] Output the location of calls to dump() (ktherage) - * feature #33412 [Console] Do not leak hidden console commands (m-vo) - * feature #33676 [Security] add "anonymous: lazy" mode to firewalls (nicolas-grekas) - * feature #32440 [DomCrawler] add a normalizeWhitespace argument to text() method (Simperfit) - * feature #33148 [Intl] Excludes locale from language codes (split localized language names) (ro0NL) - * feature #31202 [FrameworkBundle] WebTestCase KernelBrowser::getContainer null return type (Simperfit) - * feature #33038 [ErrorHandler] Forward \Throwable (fancyweb) - * feature #33574 [Http][DI] Replace REMOTE_ADDR in trusted proxies with the current REMOTE_ADDR (mcfedr) - * feature #33113 [Messenger][DX] Display real handler if handler is wrapped (DavidBadura) - * feature #33128 [FrameworkBundle] Sort tagged services (krome162504) - * feature #33658 [Yaml] fix parsing inline YAML spanning multiple lines (xabbuh) - * feature #33698 [HttpKernel] compress files generated by the profiler (nicolas-grekas) - * feature #33317 [Messenger] Added support for `from_transport` attribute on `messenger.message_handler` tag (ruudk) - * feature #33584 [Security] Deprecate isGranted()/decide() on more than one attribute (wouterj) - * feature #33663 [Security] Make stateful firewalls turn responses private only when needed (nicolas-grekas) - * feature #33609 [Form][SubmitType] Add "validate" option (fancyweb) - * feature #33621 Revert "feature #33507 [WebProfiler] Deprecated intercept_redirects in 4.4 (dorumd)" (lyrixx) - * feature #33605 [Twig] Add NotificationEmail (fabpot) - * feature #33623 [DependencyInjection] Allow binding iterable and tagged services (lyrixx) - * feature #33507 [WebProfiler] Deprecated intercept_redirects in 4.4 (dorumd) - * feature #33579 Adding .gitattributes to remove Tests directory from "dist" (Nyholm) - * feature #33562 [Mailer] rename SmtpEnvelope to Envelope (xabbuh) - * feature #33565 [Mailer] Rename an exception class (fabpot) - * feature #33516 [Cache] Added reserved characters constant for CacheItem (andyexeter) - * feature #33503 [SecurityBundle] Move Anonymous DI integration to new AnonymousFactory (wouterj) - * feature #33535 [WebProfilerBundle] Assign automatic colors to custom Stopwatch categories (javiereguiluz) - * feature #32565 [HttpClient] Allow enabling buffering conditionally with a Closure (rjwebdev) - * feature #32032 [DI] generate preload.php file for PHP 7.4 in cache folder (nicolas-grekas) - * feature #33117 [FrameworkBundle] Added --sort option for TranslationUpdateCommand (k0d3r1s) - * feature #32832 [Serializer] Allow multi-dimenstion object array in AbstractObjectNormalizer (alediator) - * feature #33189 New welcome page on startup for 4.4 LTS & 5.0 (yceruto) - * feature #33295 [OptionsResolver] Display full nested option hierarchy in exceptions (fancyweb) - * feature #33486 [VarDumper] Display fully qualified title (pavinthan, nicolas-grekas) - * feature #33496 Deprecated not passing dash symbol (-) to STDIN commands (yceruto) - * feature #32742 [Console] Added support for definition list and horizontal table (lyrixx) - * feature #33494 [Mailer] Change DSN syntax (fabpot) - * feature #33471 [Mailer] Check email validity before opening an SMTP connection (fabpot) - * feature #31177 #21571 Comparing roles to detected that users has changed (oleg-andreyev) - * feature #33459 [Validator] Deprecated CacheInterface in favor of PSR-6 (derrabus) - * feature #33271 Added new ErrorController + Preview and enabling there the error renderer mechanism (yceruto) - * feature #33454 [Mailer] Improve an exception when trying to send a RawMessage without an Envelope (fabpot) - * feature #33327 [ErrorHandler] Registering basic exception handler for late failures (yceruto) - * feature #33446 [TwigBridge] lint all templates from configured Twig paths if no argument was provided (yceruto) - * feature #33409 [Mailer] Add support for multiple mailers (fabpot) - * feature #33424 [Mailer] Change the DSN semantics (fabpot) - * feature #33319 Allow configuring class names through methods instead of class parameters in Doctrine extensions (alcaeus) - * feature #33283 [ErrorHandler] make DebugClassLoader able to add return type declarations (nicolas-grekas) - * feature #33323 [TwigBridge] Throw an exception when one uses email as a context variable in a TemplatedEmail (fabpot) - * feature #33308 [SecurityGuard] Deprecate returning non-boolean values from checkCredentials() (derrabus) - * feature #33217 [FrameworkBundle][DX] Improving the redirect config when using RedirectController (yceruto) - * feature #33015 [HttpClient] Added TraceableHttpClient and WebProfiler panel (jeremyFreeAgent) - * feature #33091 [Mime] Add Address::fromString (gisostallenberg) - * feature #33144 [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml() (lyrixx) - * feature #33152 Mark all dispatched event classes as final (Tobion) - * feature #33258 [HttpKernel] deprecate global dir to load resources from (Tobion) - * feature #33272 [Translation] deprecate support for null locales (xabbuh) - * feature #33269 [TwigBridge] Mark all classes extending twig as @final (fabpot) - * feature #33270 [Mime] Remove NamedAddress (fabpot) - * feature #33169 [HttpFoundation] Precalculate session expiry timestamp (azjezz) - * feature #33237 [Mailer] Remove the auth mode DSN option and support in the eSMTP transport (fabpot) - * feature #33233 [Mailer] Simplify the way TLS/SSL/STARTTLS work (fabpot) - * feature #32360 [Monolog] Added ElasticsearchLogstashHandler (lyrixx) - * feature #32489 [Messenger] Allow exchange type headers binding (CedrickOka) - * feature #32783 [Messenger] InMemoryTransport handle acknowledged and rejected messages (tienvx) - * feature #33155 [ErrorHandler] Added call() method utility to turns any PHP error into \ErrorException (yceruto) - * feature #33203 [Mailer] Add support for the queued flag in the EmailCount assertion (fabpot) - * feature #30323 [ErrorHandler] trigger deprecation in DebugClassLoader when child class misses a return type (fancyweb, nicolas-grekas) - * feature #33137 [DI] deprecate support for non-object services (nicolas-grekas) - * feature #32845 [HttpKernel][FrameworkBundle] Add alternative convention for bundle directories (yceruto) - * feature #32548 [Translation] XliffLintCommand: allow .xliff file extension (codegain) - * feature #28363 [Serializer] Encode empty objects as objects, not arrays (mcfedr) - * feature #33122 [WebLink] implement PSR-13 directly (nicolas-grekas) - * feature #33078 Add compatibility trait for PHPUnit constraint classes (alcaeus) - * feature #32988 [Intl] Support ISO 3166-1 Alpha-3 country codes (terjebraten-certua) - * feature #32598 [FrameworkBundle][Routing] Private service route loaders (fancyweb) - * feature #32486 [DoctrineBridge] Invokable event listeners (fancyweb) - * feature #31083 [Validator] Allow objects implementing __toString() to be used as violation messages (mdlutz24) - * feature #32122 [HttpFoundation] deprecate HeaderBag::get() returning an array and add all($key) instead (Simperfit) - * feature #32807 [HttpClient] add "max_duration" option (fancyweb) - * feature #31546 [Dotenv] Use default value when referenced variable is not set (j92) - * feature #32930 [Mailer][Mime] Add PHPUnit constraints and assertions for the Mailer (fabpot) - * feature #32912 [Mailer] Add support for the profiler (fabpot) - * feature #32940 [PhpUnitBridge] Add polyfill for PhpUnit namespace (jderusse) - * feature #31843 [Security] add support for opportunistic password migrations (nicolas-grekas) - * feature #32824 [Ldap] Add security LdapUser and provider (chalasr) - * feature #32922 [PhpUnitBridge] make the bridge act as a polyfill for newest PHPUnit features (nicolas-grekas) - * feature #32927 [Mailer] Add message events logger (fabpot) - * feature #32916 [Mailer] Add a name to the transports (fabpot) - * feature #32917 [Mime] Add AbstractPart::asDebugString() (fabpot) - * feature #32543 [FrameworkBundle] add config translator cache_dir (Raulnet) - * feature #32669 [Yaml] Add flag to dump NULL as ~ (OskarStark) - * feature #32896 [Mailer] added debug info to TransportExceptionInterface (fabpot) - * feature #32817 [DoctrineBridge] Deprecate RegistryInterface (Koc) - * feature #32504 [ErrorRenderer] Add DebugCommand for easy debugging and testing (yceruto) - * feature #32581 [DI] Allow dumping the container in one file instead of many files (nicolas-grekas) - * feature #32762 [Form][DX] derive default timezone from reference_date option when possible (yceruto) - * feature #32745 [Messenger][Profiler] Attempt to give more useful source info when using HandleTrait (ogizanagi) - * feature #32680 [Messenger][Profiler] Collect the stamps at the end of dispatch (ogizanagi) - * feature #32683 [VarDumper] added support for Imagine/Image (lyrixx) - * feature #32749 [Mailer] Make transport factory test case public (Koc) - * feature #32718 [Form] use a reference date to handle times during DST (xabbuh) - * feature #32637 [ErrorHandler] Decouple from ErrorRenderer component (yceruto) - * feature #32609 [Mailer][DX][RFC] Rename mailer bridge transport classes (Koc) - * feature #32587 [Form][Validator] Generate accept attribute with file constraint and mime types option (Coosos) - * feature #32658 [Form] repeat preferred choices in list of all choices (Seb33300, xabbuh) - * feature #32698 [WebProfilerBundle] mark all classes as internal (Tobion) - * feature #32695 [WebProfilerBundle] Decoupling TwigBundle and using the new ErrorRenderer mechanism (yceruto) - * feature #31398 [TwigBundle] Deprecating error templates for non-html formats and using ErrorRenderer as fallback (yceruto) - * feature #32582 [Routing] Deprecate ServiceRouterLoader and ObjectRouteLoader in favor of ContainerLoader and ObjectLoader (fancyweb) - * feature #32661 [ErrorRenderer] Improving the exception page provided by HtmlErrorRenderer (yceruto) - * feature #32332 [DI] Move non removing compiler passes to after removing passes (alexpott) - * feature #32475 [Process] Deprecate Process::inheritEnvironmentVariables() (ogizanagi) - * feature #32583 [Mailer] Logger vs debug mailer (fabpot) - * feature #32471 Add a new ErrorHandler component (mirror of the Debug component) (yceruto) - * feature #32463 [VarDumper] Allow to configure VarDumperTestTrait casters & flags (ogizanagi) - * feature #31946 [Mailer] Extract transport factory and allow create custom transports (Koc) - * feature #31194 [PropertyAccess] Improve errors when trying to find a writable property (pierredup) - * feature #32435 [Validator] Add a new constraint message when there is both min and max (Lctrs) - * feature #32470 Rename ErrorCatcher to ErrorRenderer (rendering part only) (yceruto) - * feature #32462 [WebProfilerBundle] Deprecating templateExists method (yceruto) - * feature #32446 [Lock] rename and deprecate Factory into LockFactory (Simperfit) - * feature #31975 Dynamic bundle assets (garak) - * feature #32429 [VarDumper] Let browsers trigger their own search on double CMD/CTRL + F (ogizanagi) - * feature #32198 [Lock] Split "StoreInterface" into multiple interfaces with less responsability (Simperfit) - * feature #31511 [Validator] Allow to use property paths to get limits in range constraint (Lctrs) - * feature #32424 [Console] don't redraw progress bar more than every 100ms by default (nicolas-grekas) - * feature #32418 [Console] Added Application::reset() (lyrixx) - * feature #31217 [WebserverBundle] Deprecate the bundle in favor of symfony local server (Simperfit) - * feature #31554 [SECURITY] AbstractAuthenticationListener.php error instead info. Rebase of #28462 (berezuev) - * feature #32284 [Cache] Add argument $prefix to AdapterInterface::clear() (nicolas-grekas) - * feature #32423 [ServerBundle] Display all logs by default (lyrixx) - * feature #26339 [Console] Add ProgressBar::preventRedrawFasterThan() and forceRedrawSlowerThan() methods (ostrolucky) - * feature #31269 [Translator] Dump native plural formats to po files (Stadly) - * feature #31560 [Ldap][Security] LdapBindAuthenticationProvider does not bind before search query (Simperfit) - * feature #31626 [Console] allow answer to be trimmed by adding a flag (Simperfit) - * feature #31876 [WebProfilerBundle] Add clear button to ajax tab (Matts) - * feature #32415 [Translation] deprecate passing a null locale (Simperfit) - * feature #32290 [HttpClient] Add $response->toStream() to cast responses to regular PHP streams (nicolas-grekas) - * feature #32402 [Intl] Exclude root language (ro0NL) - * feature #32295 [FrameworkBundle] Add autowiring alias for PSR-14 (nicolas-grekas) - * feature #32390 [DependencyInjection] Deprecated passing Parameter instances as class name to Definition (derrabus) - * feature #32106 [FrameworkBundle] Use default_locale as default value for translator.fallbacks (dunglas) - * feature #32294 [FrameworkBundle] Allow creating chained cache pools by providing several adapters (nicolas-grekas) - * feature #32207 [FrameworkBundle] Allow to use the BrowserKit assertions with Panther and API Platform's test client (dunglas) - * feature #32344 [HttpFoundation][HttpKernel] Improving the request/response format autodetection (yceruto) - * feature #32231 [HttpClient] Add support for NTLM authentication (nicolas-grekas) - * feature #32265 [Validator] deprecate non-string constraint violation codes (xabbuh) - * feature #31528 [Validator] Add a Length::$allowEmptyString option to reject empty strings (ogizanagi) - * feature #32081 [WIP][Mailer] Overwrite envelope sender and recipients from config (Devristo) - * feature #32255 [HttpFoundation] Drop support for ApacheRequest (lyrixx) - * feature #31825 [Messenger] Added support for auto trimming of redis streams (Toflar) - * feature #32277 Remove @experimental annotations (fabpot) - * feature #30981 [Mime] S/MIME Support (sstok) - * feature #32180 [Lock] add an InvalidTTLException to be more accurate (Simperfit) - * feature #32241 [PropertyAccess] Deprecate null as allowed value for defaultLifetime argument in createCache method (jschaedl) - * feature #32221 [ErrorCatcher] Make IDEs and static analysis tools happy (fabpot) - * feature #32227 Rename the ErrorHandler component to ErrorCatcher (fabpot) - * feature #31065 Add ErrorHandler component (yceruto) - * feature #32126 [Process] Allow writing portable "prepared" command lines (Simperfit) - * feature #31532 [Ldap] Add users extraFields in ldap component (Simperfit) - * feature #32104 Add autowiring for HTTPlug (nicolas-grekas) - * feature #32130 [Form] deprecate int/float for string input in NumberType (xabbuh) - * feature #31547 [Ldap] Add exception for mapping ldap errors (Simperfit) - * feature #31764 [FrameworkBundle] add attribute stamps (walidboughdiri) - * feature #32059 [PhpUnitBridge] Bump PHPUnit 7+8 (ro0NL) - * feature #32041 [Validator] Deprecate unused arg in ExpressionValidator (ogizanagi) - * feature #31287 [Config] Introduce find method in ArrayNodeDefinition to ease configuration tree manipulation (jschaedl) - * feature #31959 [DomCrawler][Feature][DX] Add Form::getName() method (JustBlackBird) - * feature #32026 [VarDumper] caster for HttpClient's response dumps all info (nicolas-grekas) - * feature #31976 [HttpClient] add HttplugClient for compat with libs that need httplug v1 or v2 (nicolas-grekas) - * feature #31956 [Mailer] Changed EventDispatcherInterface dependency from Component to Contracts (Koc) - * feature #31980 [HttpClient] make Psr18Client implement relevant PSR-17 factories (nicolas-grekas) - * feature #31919 [WebProfilerBundle] Select default theme based on user preferences (javiereguiluz) - * feature #31451 [FrameworkBundle] Allow dots in translation domains (jschaedl) - * feature #31321 [DI] deprecates tag !tagged in favor of !tagged_iterator (jschaedl) - * feature #31658 [HTTP Foundation] Deprecate passing argument to method Request::isMethodSafe() (dFayet) - * feature #31597 [Security] add MigratingPasswordEncoder (nicolas-grekas) - * feature #31351 [Validator] Improve TypeValidator to handle array of types (jschaedl) - * feature #31526 [Validator] Add compared value path to violation parameters (ogizanagi) - * feature #31514 Add exception as HTML comment to beginning and end of `exception_full.html.twig` (ruudk) - * feature #31739 [FrameworkBundle] Add missing BC layer for deprecated ControllerNameParser injections (chalasr) - * feature #31831 [HttpClient] add $response->cancel() (nicolas-grekas) - * feature #31334 [Messenger] Add clear Entity Manager middleware (Koc) - * feature #31594 [Security] add PasswordEncoderInterface::needsRehash() (nicolas-grekas) - * feature #31821 [FrameworkBundle][TwigBundle] Add missing deprecations for PHP templating layer (yceruto) - * feature #31509 [Monolog] Setup the LoggerProcessor after all other processor (lyrixx) - * feature #31785 [Messenger] Deprecate passing a bus locator to ConsumeMessagesCommand's constructor (chalasr) - * feature #31700 [MonologBridge] RouteProcessor class is now final to ease the the removal of deprecated event (Simperfit) - * feature #31732 [HttpKernel] Make DebugHandlersListener internal (chalasr) - * feature #31539 [HttpKernel] Add lts config (noniagriconomie) - * feature #31437 [Cache] Add Redis Sentinel support (StephenClouse) - * feature #31543 [DI] deprecate short callables in yaml (nicolas-grekas) - diff --git a/CHANGELOG-6.0.md b/CHANGELOG-6.0.md new file mode 100644 index 0000000000000..f92727fee106c --- /dev/null +++ b/CHANGELOG-6.0.md @@ -0,0 +1,563 @@ +CHANGELOG for 6.0.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 6.0 minor versions. + +To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash +To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.0.0...v6.0.1 + +* 6.0.6 (2022-03-05) + + * bug #45619 [redis-messenger] remove undefined array key warnings (PhilETaylor) + * bug #45637 [Cache] do not pass DBAL connections to PDO adapters (xabbuh) + * bug #45631 [HttpFoundation] Fix PHP 8.1 deprecation in `Response::isNotModified` (HypeMC) + * bug #45610 [HttpKernel] Guard against bad profile data (nicolas-grekas) + * bug #45532 Fix deprecations on PHP 8.2 (nicolas-grekas) + * bug #45595 [FrameworkBundle] Fix resetting container between tests (nicolas-grekas) + * bug #45590 [Console] Revert StringInput bc break from #45088 (bobthecow) + * bug #45585 [HttpClient] fix checking for unset property on PHP <= 7.1.4 (nicolas-grekas) + * bug #45583 [WebProfilerBundle] Fixes HTML syntax regression introduced by #44570 (xavismeh) + +* 6.0.5 (2022-02-28) + + * bug #45351 [WebProfilerBundle] Log section minor fixes (missing "notice" filter, log priority, accessibility) (Amunak) + * bug #44967 [Validator] Multi decimal to alpha for CssColor validator (tilimac) + * bug #45546 [Console] Fix null handling in formatAndWrap() (derrabus) + * bug #44570 [WebProfilerBundle] add nonces to profiler (garak) + * bug #44839 MailerInterface: failed exception contract when enabling messenger (Giorgio Premi) + * bug #45526 [Lock] Release Locks from Internal Store on Postgres waitAndSave* (chrisguitarguy) + * bug #45529 [DependencyInjection] Don't reset env placeholders during compilation (nicolas-grekas) + * bug #45527 [HttpClient] Fix overriding default options with null (nicolas-grekas) + * bug #45531 [Serializer] Fix passing null to str_contains() (Erwin Dirks) + * bug #42458 [Validator][Tests] Fix AssertingContextualValidator not throwing on remaining expectations (fancyweb) + * bug #45279 [Messenger] Fix dealing with unexpected payload in Redis transport (nicoalonso) + * bug #45496 [VarDumper] Fix dumping mysqli_driver instances (nicolas-grekas) + * bug #45495 [HttpFoundation] Fix missing ReturnTypeWillChange attributes (luxemate) + * bug #45482 [Cache] Add missing log when saving namespace (developer-av) + * bug #45479 [HttpKernel] Reset services between requests performed by KernelBrowser (nicolas-grekas) + * bug #44650 [Serializer] Make document type nodes ignorable (boenner) + * bug #45469 [SecurityBundle] fix autoconfiguring Monolog's ProcessorInterface (nicolas-grekas) + * bug #45414 [FrameworkBundle] KernelTestCase resets internal state on tearDown (core23) + * bug #45430 [Dotenv] Fix reading config for symfony/runtime when running dump command (nicolas-grekas) + * bug #45460 [Intl] fix wrong offset timezone PHP 8.1 (Lenny4) + * bug #45462 [HttpKernel] Fix extracting controller name from closures (nicolas-grekas) + * bug #45463 [Security/Http] Fix getting password-upgrader when user-loader is a closure (nicolas-grekas) + * bug #45424 [DependencyInjection] Fix type binding (sveneld) + * bug #45426 [Runtime] Fix dotenv_overload with commands (fancyweb) + * bug #44259 [Security] AccountStatusException::$user should be nullable (Cantepie) + * bug #45391 [Serializer] Ensuring end of line character apply with constructor settings in CSV encoder (bizley) + * bug #45323 [Serializer] Fix ignored callbacks in denormalization (benjaminmal) + * bug #45399 [FrameworkBundle] Fix sorting bug in sorting of tagged services by priority (Ahummeling) + * bug #45338 [Mailer] Fix string-cast of exceptions thrown by authenticator in EsmtpTransport (wikando-ck) + * bug #45339 [Cache] fix error handling when using Redis (nicolas-grekas) + * bug #45331 [Security]  Fix wrong authenticator class in debug logs (chalasr) + * bug #45322 Fix generic type for FormErrorIterator (akalineskou) + * bug #45281 [Cache] Fix connecting to Redis via a socket file (alebedev80) + * bug #45289 [FrameworkBundle] Fix log channel of TagAwareAdapter (fancyweb) + * bug #45306 [PropertyAccessor] Add missing TypeError catch (b1rdex) + * bug #44868 [DependencyInjection][FrameworkBundle] Fix using PHP 8.1 enum as parameters (ogizanagi) + * bug #45298 [HttpKernel] Fix FileLinkFormatter with empty xdebug.file_link_format (fancyweb) + * bug #45299 [DependencyInjection] Fix AsEventListener not working on decorators (LANGERGabrielle) + * bug #45302 [HttpKernel][WebProfilerBundle] Fixed error count by log not displayed in WebProfilerBundle (SVillette) + * bug #45219 [WebProfilerBundle] Fixes weird spacing in log message context/trace output (jennevdmeer) + * bug #45290 [Notifier] fix Microsoft Teams webhook url (christophkoenig) + * bug #45274 [Mailer] allow Mailchimp to handle multiple TagHeader's (kbond) + * bug #45275 [Mailer] ensure only a single tag can be used with Postmark (kbond) + * bug #45261 [HttpClient] Fix Content-Length header when possible (nicolas-grekas) + * bug #45263 [Routing] AnnotationDirectoryLoader::load() may return null (mhujer) + * bug #45258 [DependencyInjection] Don't dump polyfilled classes in preload script (nicolas-grekas) + * bug #38534 [Serializer] make XmlEncoder stateless thus reentrant (connorhu) + * bug #42253 [Form] Do not fix URL protocol for relative URLs (bogkonstantin) + * bug #45256 [DomCrawler] ignore bad charsets (nicolas-grekas) + * bug #45255 [PropertyAccess] Fix handling of uninitialized property of parent class (filiplikavcan) + * bug #45204 [Validator] Fix minRatio and maxRatio when getting rounded (alexander-schranz) + * bug #45240 [Console] Revert StringInput bc break from #45088 (bobthecow) + * bug #45243 [DoctrineBridge] Fix compatibility with doctrine/orm 3 in Id generators (ostrolucky) + +* 6.0.4 (2022-01-29) + + * security #cve-2022-xxxx [FrameworkBundle] Enable CSRF in FORM by default (jderusse) + +* 6.0.3 (2022-01-28) + + * bug #45193 [FrameworkBundle] Fix missing arguments when a serialization default context is bound (ArnoudThibaut) + * bug #44997 [Runtime] Fix --env and --no-debug with dotenv_overload (fancyweb) + * bug #45188 [Dotenv] Fix bootEnv() override with .env.local.php when the env key already exists (fancyweb) + * bug #45095 [Finder] Fix finding VCS re-included files in excluded directory (julienfalque) + * bug #44987 [DoctrineBridge] Fix automapping (mbabker) + * bug #44860 [Validator] Fix Choice constraint with associative choices array (derrabus) + * bug #44939 [Form] UrlType should not add protocol to emails (GromNaN) + * bug #43149 Silence warnings during tty detection (neclimdul) + * bug #45154 [Serializer] Fix AbstractObjectNormalizer not considering pseudo type false (Thomas Nunninger) + * bug #45185 [Notifier] Fix encoding of messages with FreeMobileTransport (94noni) + * bug #45181 [Console] Fix PHP 8.1 deprecation in ChoiceQuestion (BrokenSourceCode) + * bug #44634 [HttpKernel] Fix compatibility with php bridge and already started php sessions (alexander-schranz) + * bug #45174 [Notifier] Use the UTF-8 encoding in smsapi-notifier (marphi) + * bug #45140 [Yaml] Making the parser stateless (mamazu) + * bug #45109 [Console] fix restoring stty mode on CTRL+C (nicolas-grekas) + * bug #45103 [Process] Avoid calling fclose on an already closed resource (Seldaek) + * bug #44941 [RateLimiter] Resolve crash on near-round timestamps (xesxen) + * bug #45088 [Console] fix parsing escaped chars in StringInput (nicolas-grekas) + * bug #45096 [Cache] Throw exception if incompatible version of psr/simple-cache is used (colinodell) + * bug #45067 [RateLimiter] Implicit conversion fix (brian978) + * bug #45063 [DependencyInjection] remove arbitratry limitation to exclude inline services from bindings (nicolas-grekas) + * bug #44986 [DependencyInjection] copy synthetic status when resolving child definitions (kbond) + * bug #45073 [HttpClient] Fix Failed to open stream: Too many open files (adrienfr) + * bug #45053 [Console] use STDOUT/ERR in ConsoleOutput to save opening too many file descriptors (nicolas-grekas) + * bug #45029 [Cache] Set mtime of cache files 1 year into future if they do not expire (Blacksmoke16) + * bug #45012 [DoctrineBridge] Fix invalid guess with enumType (jderusse) + * bug #45015 [HttpClient] fix resetting DNS/etc when calling CurlHttpClient::reset() (nicolas-grekas) + * bug #45004 [HttpClient] Remove deprecated usage of GuzzleHttp\Promise\promise_for (plozmun) + * bug #44998 [FrameworkBundle] Allow default cache pools to be overwritten by user (Seldaek) + * bug #44890 [HttpClient] Remove deprecated usage of `GuzzleHttp\Promise\queue` (GrahamCampbell) + * bug #45002 [PropertyAccess] Fix handling of uninitialized property of anonymous class (filiplikavcan) + * bug #44979 [DependencyInjection] Add iterable to possible binding type (vladimir.panivko) + * bug #44908 [Serializer] Fix AbstractObjectNormalizer TypeError on denormalization (JustDylan23) + * bug #44976 [FrameworkBundle] Avoid calling rtrim(null, '/') in AssetsInstallCommand (pavol-tk, GromNaN) + * bug #44879 [DependencyInjection] Ignore argument type check in CheckTypeDeclarationsPass if it's a Definition with a factory (fancyweb) + * bug #44920 Use correct tag for ExpoTransportFactory service (jschaedl) + * bug #44931 Allow a zero time-limit for messenger:consume (fritzmg) + * bug #44932 [DependencyInjection] Fix nested env var with resolve processor (Laurent Moreau) + * bug #44912 [Console] Allow OutputFormatter::escape() to be used for escaping URLs used in (Seldaek) + * bug #44877 [Validator] Error using CssColor with doctrine annotations (sormes) + * bug #44878 [HttpClient] Turn negative timeout to a very long timeout (fancyweb) + * bug #44854 [Validator] throw when Constraint::_construct() has not been called (nicolas-grekas) + * bug #44857 [Translation] [LocoProvider] Fix use of asset ids (danut007ro) + +* 6.0.2 (2021-12-29) + + * bug #44828 [Lock] Release DoctrineDbalPostgreSqlStore connection lock on failure (simon-watiau) + * bug #44838 [DependencyInjection][HttpKernel] Fix enum typed bindings (ogizanagi) + * bug #44723 [Lock] Release PostgreSqlStore connection lock on failure (simon-watiau) * commit 'e5b2f9efba': [Lock] Release PostgreSqlStore connection lock on failure + * bug #44826 [HttpKernel] Do not attempt to register enum arguments in controller service locator (ogizanagi) + * bug #44822 [Mime][Security] Fix missing sprintf and add tests (alamirault) + * bug #44824 [Mime] Fix missing sprintf in DkimSigner (alamirault) + * bug #44816 [Translation] [LocoProvider] Use rawurlencode and separate tag setting (danut007ro) + * bug #44805 [Security] fix unserializing session payloads from v4 (nicolas-grekas) + * bug #44820 [Cache] Don't lock when doing nested computations (nicolas-grekas) + * bug #44807 [Messenger] fix Redis support on 32b arch (nicolas-grekas) + * bug #44759 [HttpFoundation] Fix notice when HTTP_PHP_AUTH_USER passed without pass (Vitali Tsyrkin) + * bug #44809 [WebProfilerBundle] relax return type for memory data collector (94noni) + * bug #44799 [Cache] fix compat with apcu < 5.1.10 (nicolas-grekas) + * bug #44764 [Form] Expand FormView key to include int (biozshock) + * bug #44730 [Console] Fix autocompletion of argument with default value (GromNaN) + * bug #44637 [PropertyInfo] PhpStan extractor nested object fix (rmikalkenas) + * bug #44085 [Translation] Fix TranslationPullCommand with ICU translations (Kocal) + * bug #44578 [PropertyInfo] Fix phpstan extractor issues (ostrolucky) + * bug #44771 [Notifier] Use correct factory for the msteams transport (veewee) + * bug #44618 [HttpKernel] Fix SessionListener without session in request (shyim) + * bug #44743 [HttpClient] fix checking for recent curl consts (nicolas-grekas) + * bug #44752 [Security/Http] Fix cookie clearing on logout (maxhelias) + * bug #44745 [EventDispatcher][HttpFoundation] Restore return type to covariant IteratorAggregate implementations (derrabus) + * bug #44732 [Mime] Relaxing in-reply-to header validation (ThomasLandauer) + * bug #44714 [WebProfilerBundle] fix Email HTML preview (94noni) + * bug #44737 Fix Psr16Cache not being compatible with non-Symfony cache pools (colinodell) + * bug #44728 [Mime] Fix encoding filenames in multipart/form-data (nicolas-grekas) + * bug #44602 [Serializer] Improve UidNormalizer denormalize error message (fancyweb) + * bug #44383 [Lock] Create tables in transaction only if supported by driver (martinssipenko) + * bug #44518 [HttpFoundation] Take php session.cookie settings into account (simonchrz) + * bug #44719 [ErrorHandler] fix on patching return types on Windows (nicolas-grekas) + * bug #44710 [DependencyInjection] fix linting callable classes (nicolas-grekas) + * bug #44639 [DependencyInjection] Cast tag attribute value to string (ruudk) + * bug #44473 [Validator] Restore default locale in ConstraintValidatorTestCase (rodnaph) + * bug #44682 [FrameworkBundle] alias `cache.app.taggable` to `cache.app` if using `cache.adapter.redis_tag_aware` (kbond) + * bug #44649 [HttpKernel] fix how configuring log-level and status-code by exception works (nicolas-grekas) + * bug #44667 [Cache] Revert "feature #41989 make `LockRegistry` use semaphores when possible" (nicolas-grekas) + * bug #44671 [HttpClient] Fix tracing requests made after calling withOptions() (nicolas-grekas) + * bug #44577 [Cache] Fix proxy no expiration to the Redis (Sergey Belyshkin) + * bug #44669 [Cache] disable lock on CLI (nicolas-grekas) + * bug #44598 [Translation] Handle the blank-translation in Loco Adapter (kgonella) + * bug #44448 [Validator] Allow Sequence constraint to be applied onto class as an attribute (sidz) + * bug #44354 [RateLimiter] Make RateLimiter resilient to timeShifting (jderusse) + * bug #44600 [Serializer] Fix denormalizing custom class in UidNormalizer (fancyweb) + * bug #44537 [Config] In XmlUtils, avoid converting from octal every string starting with a 0 (alexandre-daubois) + * bug #44510 [Workflow] Fix eventsToDispatch parameter setup for StateMachine (Olexandr Kalaidzhy) + * bug #44625 [HttpClient] fix monitoring responses issued before reset() (nicolas-grekas) + * bug #44623 [HttpClient] Fix dealing with "HTTP/1.1 000 " responses (nicolas-grekas) + * bug #44430 [PropertyInfo] Fix aliased namespace matching (Korbeil) + * bug #44601 [HttpClient] Fix closing curl-multi handle too early on destruct (nicolas-grekas) + * bug #44554 Make enable_authenticator_manager true as there is no other way in Symfony 6 (alexander-schranz) + * bug #44571 [HttpClient] Don't reset timeout counter when initializing requests (nicolas-grekas) + * bug #44479 [HttpClient] Double check if handle is complete (Nyholm) + * bug #44418 [DependencyInjection] Resolve ChildDefinition in AbstractRecursivePass (fancyweb) + * bug #44474 [Translation] [Bridge] [Lokalise] Fix push keys to lokalise. Closes #… (olegmifle) + * bug #43164 [FrameworkBundle] Fix cache pool configuration with one adapter and one provider (fancyweb) + * bug #44419 [PropertyAccess] Fix accessing public property on Object (kevcomparadise) + * bug #44565 [FrameworkBundle] Use correct cookie domain in loginUser() (wouterj) + * bug #44538 [Process] fixed uppercase ARGC and ARGV should also be skipped (rbaarsma) + * bug #44438 [HttpClient] Fix handling thrown \Exception in \Generator in MockResponse (fancyweb) + * bug #44469 [String] Fix requiring wcswitch table several times (fancyweb) + * bug #44428 [HttpClient] Fix response id property check in MockResponse (fancyweb) + * bug #44539 [Lock] Fix missing argument in PostgreSqlStore::putOffExpiration with DBAL connection (GromNaN) + +* 6.0.1 (2021-12-09) + + * bug #44494 Remove FQCN type hints on properties (fabpot) + * bug #44490 [DependencyInjection][Messenger] Add auto-registration for BatchHandlerInterface (GaryPEGEOT) + * bug #44523 [Console] Fix polyfill-php73 requirement (Seldaek) + * bug #44514 Don't access uninitialized typed property ChromePhpHandler::$response (Philipp91) + * bug #44502 [HttpFoundation] do not call preg_match() on null (xabbuh) + * bug #44475 [Console] Handle alias in completion script (GromNaN) + * bug #44481 [FrameworkBundle] Fix loginUser() causing deprecation (wouterj) + * bug #44416 [Translation] Make http requests synchronous when reading the Loco API (Kocal) + * bug #44437 [HttpKernel] Fix wrong usage of SessionUtils::popSessionCookie (simonchrz) + * bug #44350 [Translation] Fix TranslationTrait (Tomasz Kusy) + * bug #44460 [SecurityBundle] Fix ambiguous deprecation message on missing provider (chalasr) + * bug #44467 [Console] Fix parameter types for `ProcessHelper::mustRun()` (derrabus) + * bug #44427 [FrameworkBundle] Fix compatibility with symfony/security-core 6.x (deps=high tests) (wouterj) + * bug #44424 [SecurityBundle] Don't rely on deprecated strategy constants (derrabus) + * bug #44399 Prevent infinite nesting of lazy `ObjectManager` instances when `ObjectManager` is reset (Ocramius) + * bug #44402 [HttpKernel] Fix using FileLinkFormatter after serialization (derrabus) + * bug #44395 [HttpKernel] fix sending Vary: Accept-Language when appropriate (nicolas-grekas) + * bug #44385 [DependencyInjection] Skip parameter attribute configurators in AttributeAutoconfigurationPass if we can't get the constructor reflector (fancyweb) + * bug #44359 Avoid duplicated session listener registration in tests (alexander-schranz) + * bug #44375 [DoctrineBridge] fix calling get_class on non-object (kbond) + * bug #44378 [HttpFoundation] fix SessionHandlerFactory using connections (dmaicher) + * bug #44365 [SecurityBundle]  Fix invalid reference with `always_authenticate_before_granting` (chalasr) + * bug #44361 [HttpClient] Fix handling error info in MockResponse (fancyweb) + * bug #44370 [Lock] create lock table if it does not exist (martinssipenko) + +* 6.0.0 (2021-11-29) + + * bug #44309 [Messenger] Leverage DBAL's getNativeConnection() method (derrabus) + * bug #44300 [FrameworkBundle] Fix property-info phpstan extractor discovery (1ed) + * feature #44271 [Notifier] add Vonage bridge to replace the Nexmo one (nicolas-grekas) + * bug #44187 [Translation] [Loco] Fix idempotency of LocoProvider write method (welcoMattic) + * bug #43992 [Security] Do not overwrite already stored tokens for REMOTE_USER authentication (stlrnz) + * bug #43876 [Validator] Fix validation for single level domains (HypeMC) + * bug #44327 [Debug][ErrorHandler] Increased the reserved memory from 10k to 32k (sakalys) + * bug #44261 [Process] intersect with getenv() in case-insensitive manner to get default envs (stable-staple) + * bug #44295 [Serializer] fix support for lazy/unset properties (nicolas-grekas) + * bug #44277 [Notifier] Fix AllMySms bridge body content (afiocre) + * bug #44269 [DoctrineBridge] Revert " add support for the JSON type" (dunglas) + +* 6.0.0-RC1 (2021-11-24) + + * security #cve-2021-41268 [SecurityBundle] Default signature_properties to the previous behavior (wouterj) + * security #cve-2021-41267 [HttpKernel] Fix missing extra trusted header in sub-request (jderusse) + * security #cve-2021-41270 [Serializer] Use single quote to escape formulas (jderusse) + * bug #44230 [Console] Add Suggestion class for more advanced completion suggestion (wouterj) + * bug #44232 [Cache] fix connecting to local Redis sockets (nicolas-grekas) + * bug #44204 [HttpClient] fix closing curl multi handle when destructing client (nicolas-grekas) + * bug #44208 [Process] exclude argv/argc from possible default env vars (nicolas-grekas) + * bug #44188 [VarExporter] fix exporting declared but unset properties when __sleep() is implemented (nicolas-grekas) + * bug #44176 [Console] Default ansi option to null (jderusse) + * bug #44179 [WebProfilerBundle] Fix JS error when toolbar is reloaded (jderusse) + * bug #44177 [SecurityBundle] Remove Guard (derrabus) + * bug #44172 [Security] Guard is incompatible with Symfony 6 (derrabus) + * bug #44119 [HttpClient][Mime] Add correct IDN flags for IDNA2008 compliance (j-bernard) + * bug #44139 [WebProfilerBundle] Prevent installation of incompatible mailer component versions (Anne-Julia Seitz) + * bug #43917 Allow autodetecting mapping type for any object (franmomu) + * bug #44130 [SecurityBundle] Remove outdated conditions based on authenticatorManagerEnabled (chalasr) + * bug #44131 [Yaml] properly parse quoted strings tagged with !!str (xabbuh) + * bug #42323 [TwigBridge] do not merge label classes into expanded choice labels (xabbuh) + +* 6.0.0-BETA3 (2021-11-18) + + * feature #44125 Add a setter on DateTimeNormalizer to change the default context at runtime (Seldaek) + * bug #44110 [FrameworkBundle] Fix default PHP attributes support in validation and serializer configuration when doctrine/annotations is not installed with PHP 8 (fancyweb) + * bug #44115 [WebProfilerBundle] Tweak the colors of the security panel (javiereguiluz) + * bug #44121 [Serializer] fix support for lazy properties (nicolas-grekas) + * bug #44108 [FrameworkBundle][Messenger] remove `FlattenExceptionNormalizer` definition if serializer not available (kbond) + * bug #44111 [Serializer] fix support for unset properties on PHP < 7.4 (nicolas-grekas) + * bug #44098 [DependencyInjection] fix preloading (nicolas-grekas) + * bug #44065 [FrameworkBundle] Add framework config for DBAL cache adapter (GromNaN) + * bug #44096 Make ExpressionVoter Cacheable (jderusse) + * bug #44070 [Process] intersect with getenv() to populate default envs (nicolas-grekas) + * feature #43181 Allow AbstractDoctrineExtension implementations to support the newer bundle structure (mbabker) + * bug #44060 [Cache] Fix calculate ttl in couchbase sdk 3.0 (ajcerezo) + * bug #43990 [Translation] [Loco] Generate id parameter instead of letting Loco do it (welcoMattic) + * bug #44043 [Cache] fix dbindex Redis (a1812) + * feature #44015 [Cache] Decrease the probability of invalidation loss on tag eviction (nicolas-grekas) + * bug #44064 [Cache] fix releasing not acquired locks (nicolas-grekas) + * bug #44063 [DependencyInjection] fix creating 2nd container instances (nicolas-grekas) + * bug #44056 [DependencyInjection] Fix YamlFileLoader return type (1ed) + +* 6.0.0-BETA2 (2021-11-14) + + * bug #44051 [Notifier] Fix package name (fabpot) + * bug #44050 [Notifier] Fix package names (fabpot) + * bug #44042 Fix DateIntervalToStringTransformer::transform() doc (BenMorel) + * bug #44034 [Yaml] don't try to replace references in quoted strings (xabbuh) + * bug #44013 [ErrorHandler] fix parsing ``@param`` with dollars in the description (nicolas-grekas) + * bug #44010 [DependencyInjection] fix auto-refresh when inline_factories is enabled (nicolas-grekas) + * bug #44028 [ErrorHandler] Fix FlattenException::setPrevious argument typing (welcoMattic) + * bug #44016 [SecurityBundle] Fix listing listeners in profiler when authenticator manager is disabled (94noni) + * bug #44012 [DependencyInjection] fix inlining when non-shared services are involved (nicolas-grekas) + * bug #44002 [Cache] Fix Memory leak (a1812) + * bug #43993 [FrameworkBundle] fix deprecation message (nicolas-grekas) + * feature #43985 [HttpClient] Implement ResetInterface for all http clients (rmikalkenas) + * bug #43981 [FrameworkBundle] fix registering late resettable services (nicolas-grekas) + * bug #43988 [DoctrineBridge] add support for the JSON type (dunglas) + * bug #43987 [PhpUnitBridge] Fix Uncaught ValueError (dunglas) + * feature #43983 [HttpKernel] allow ignoring kernel.reset methods that don't exist (nicolas-grekas) + * bug #43967 [Loco] Fix Loco Provider ID and pull & push local messages reading (welcoMattic) + * bug #43961 [HttpClient] Curl http client has to reinit curl multi handle on reset (rmikalkenas) + * bug #43930 [DependencyInjection] Fix support for unions/intersections together with `ServiceSubscriberInterface` (kbond) + * bug #43948 [Asset][Security] Fixed leftover deprecations PHP 8.1 (michaljusiega) + * bug #43944 [Yaml] revert using functions provided by polyfill packages (xabbuh) + * bug #43940 [FrameworkBundle] Fix logic in workflow:dump between workflow name and workflow id (noniagriconomie) + * bug #43947 [HttpKernel] Make sure FileLinkFormatter can be serialized (derrabus) + * bug #43945 [Runtime] fix defining APP_DEBUG when Dotenv is not enabled (nicolas-grekas) + * bug #43946 [HttpKernel] Make sure a serialized DumpDataCollector can be unserialized (derrabus) + +* 6.0.0-BETA1 (2021-11-05) + + * feature #43916 [PropertyInfo] Support the list pseudo-type (derrabus) + * feature #43850 Add completion for DebugConfig name and path arguments (eclairia, Adrien Jourdier) + * feature #43838 feat: add completion for DebugAutowiring search argument (eclairia, Adrien Jourdier) + * feature #38464 [Routing] Add support for aliasing routes (Lctrs) + * feature #43923 [Console] Open CompleteCommand for custom outputs (wouterj) + * feature #43663 [Messenger] Add command completion for failed messages (scyzoryck) + * feature #43857 [Framework] Add completion to debug:container (GromNaN) + * feature #43891 [Messenger] Add completion to command messenger:consume (GromNaN) + * feature #42471 Add generic types to traversable implementations (derrabus) + * feature #43898 [Security] Make the abstract Voter class implement CacheableVoterInterface (javiereguiluz) + * feature #43848 [FrameworkBundle] Add completion for workflow:dump (StaffNowa) + * feature #43837 [Finder] Add .gitignore nested negated patterns support (julienfalque) + * feature #43754 Determine attribute or annotation type for directories (cinamo) + * feature #43846 Add completion for debug:twig (StaffNowa) + * feature #43138 [FrameworkBundle][HttpKernel] Add the ability to enable the profiler using a parameter (dunglas) + * feature #40457 [PropertyInfo] Add `PhpStanExtractor` (Korbeil) + * feature #40262 [DoctrineBridge] Param as connection in `*.event_subscriber/listener` tags (wbloszyk) + * feature #43354 [Messenger] allow processing messages in batches (nicolas-grekas) + * feature #43788 [DependencyInjection][FrameworkBundle][SecurityBundle][TwigBundle] Require Composer's runtime API to be present (derrabus) + * feature #43835 [SecurityBundle] Deprecate not configuring explicitly a provider for custom_authenticators when there is more than one registered provider (lyrixx) + * feature #43598 [Console] add suggestions for debug commands: firewall, form, messenger, router (IonBazan) + * feature #41993 [Security] Prevent `FormLoginAuthenticator` from responding to requests that should be handled by `JsonLoginAuthenticator` (abunch) + * feature #43751 [WebProfilerBundle] Add a "preview" tab in mailer profiler for HTML email (lyrixx) + * feature #43644 [FrameworkBundle] Add completion to debug:translation command (alexandre-daubois) + * feature #43653 [PasswordHasher] Add autocompletion for security commands (noniagriconomie) + * feature #43676 [FrameworkBundle] Add completion feature on translation:update command (stephenkhoo) + * feature #43672 [Translation] Add completion feature on translation pull and push commands (welcoMattic) + * feature #43060 [RateLimiter] Add support for long intervals (months and years) (alexandre-daubois) + * feature #42177 [Security][SecurityBundle] Implement ADM strategies as dedicated classes (derrabus) + * feature #43804 [DependencyInjection][FrameworkBundle][SecurityBundle][TwigBundle] Deprecate Composer 1 (derrabus) + * feature #43796 [Filesystem] Add third argument `$lockFile` to `Filesystem::appendToFile()` (fwolfsjaeger) + * feature #42414 [Notifier] Add Expo bridge (zairigimad) + * feature #43066 [Security] Cache voters that will always abstain (jderusse) + * feature #43758 [FrameworkBundle] Rename translation:update to translation:extract (welcoMattic) + * feature #41414 Support `statusCode` default param when loading template directly via route (dayallnash) + * feature #42238 [DependencyInjection] Add `SubscribedService` attribute, deprecate current `ServiceSubscriberTrait` usage (kbond) + * feature #38542 [FrameworkBundle][Serializer] Allow serializer default context configuration (soyuka) + * feature #43755 [Dotenv] Add $overrideExistingVars to bootEnv() and loadEnv() and dotenv_overload to SymfonyRuntime (fancyweb) + * feature #43671 add ResponseIsUnprocessable (garak) + * feature #43682 [FrameworkBundle] Add completion for config:dump-reference (StaffNowa) + * feature #43588 [Messenger] Autoconfigurable attributes (alirezamirsepassi) + * feature #43593 [Validator] Add CidrValidator to allow validation of CIDR notations (popsorin) + * feature #43683 [VarDumper] Add completion to server:dump command (alexandre-daubois) + * feature #43677 [RateLimiter] bug #42194 fix: sliding window policy to use microtime (jlekowski) + * feature #43679 [VarDumper] Add support for Fiber (lyrixx) + * feature #43680 Add suggestions for the option 'format' of lints commands: twig, yaml and xliff (makraz) + * feature #43621 Add completion for cache:pool:clear and cache:pool:delete commands (andyexeter) + * feature #43639 [Uid] Allow use autocompletion (StaffNowa) + * feature #43626 [Console] [Framework] Add completion to secrets:set and fix secrets:remove (GromNaN) + * feature #43640 [Console] Add completion to messenger:setup-transports command (Tayfun74) + * feature #43615 feat: add completion for CompletionCommand "shell" argument (dkarlovi) + * feature #43595 [Console] `SymfonyStyle` enhancements (kbond) + * feature #41268 [HttpFoundation] Allow setting session options via DSN (makraz) + * feature #43596 [Console] Add completion to help & list commands (GromNaN) + * feature #43587 [Lock] Remove support of Doctrine DBAL in PostgreSqlStore (GromNaN) + * feature #43576 [Messenger] subtract handling time from sleep time in worker (nicolas-grekas) + * feature #43585 [Lock] Remove support of Doctrine DBAL in PdoStore (GromNaN) + * feature #43386 [DependencyInjection] Extend TaggedIterator and TaggedLocator Attributes with able to specify defaultIndexMethod for #[TaggerIterator] and #[TaggedLocator] (fractalzombie) + * feature #42251 [Console] Bash completion integration (wouterj) + * feature #39402 [Notifier] Add push channel to notifier (norkunas) + * feature #43332 [Lock] Split PdoStore into DoctrineDbalStore (GromNaN) + * feature #43362 [Cache] Split PdoAdapter into DoctrineDbalAdapter (GromNaN) + * feature #43550 [HttpFoundation] Remove possibility to pass null as $requestIp in IpUtils (W0rma) + * feature #42580 [Console][FrameworkBundle] Add DotenvDebugCommand (chr-hertel) + * feature #43411 [HttpFoundation] Deprecate passing null as $requestIp in IpUtils (W0rma) + * feature #43526 Add a warning in WDT when using symfony/symfony (fabpot) + * feature #43481 [String] Add `trimSuffix()` and `trimPrefix()` methods (nicolas-grekas) + * feature #43497 [Notifier] [Twilio] Ensure from/sender is valid via regex (OskarStark) + * feature #43492 Lower log level in case of retry (jderusse) + * feature #43479 [DependencyInjection] autowire union and intersection types (nicolas-grekas) + * feature #43134 [Notifier] Add sms77 Notifier Bridge (matthiez) + * feature #43378 [HttpFoundation] Deprecate upload_progress.* and url_rewriter.tags session options (Matthew Covey) + * feature #43405 [Bridge][Monolog] Remove ResetLoggersWorkerSubscriber (lyrixx) + * feature #42582 [Security] Add authenticators info to the profiler (chalasr) + * feature #42723 [Messenger] Log when worker should stop and when `SIGTERM` is received (ruudk) + * feature #40168 [Validator] Added `CssColor` constraint (welcoMattic) + * feature #43328 [MonologBridge] Deprecate the Swiftmailer handler (fabpot) + * feature #43322 [MonologBridge] Deprecates ResetLoggersWorkerSubscriber (lyrixx) + * feature #43108 [HttpKernel] Add basic support for language negotiation (GregoireHebert) + * feature #41265 [Messenger] Add a middleware to log when transaction has been left open (lyrixx) + * feature #43280 [HttpClient] Add method to set response factory in mock client (greeflas) + * feature #42610 [Dotenv] Reimplementing symfony/flex' dump-env as a Symfony command (abdielcs, nicolas-grekas) + * feature #42244 [HttpKernel] Add support for configuring log level, and status code by exception class (lyrixx) + * feature #43236 [Security] Add alias for FirewallMapInterface to `@security`.firewall.map (lyrixx) + * feature #43150 [Finder] Add recursive .gitignore files support (julienfalque) + * feature #41608 [Runtime] Possibility to define the env and/or debug key (maxhelias) + * feature #42257 [Messenger] Allow using user's serializer for message do not fit the expected JSON structure (welcoMattic) + * feature #43148 [Cache] Throw ValueError in debug mode when serialization fails (nicolas-grekas) + * feature #43139 [Notifier] Mattermost Notifier option to post in an other channel (nathanaelmartel) + * feature #42335 [Messenger] Add `WorkerMetadata` to `Worker` class. (okwinza) + * feature #42712 [Serializer] Save missing arguments in MissingConstructorArgumentsException (BafS) + * feature #43004 [Serializer] Throw NotNormalizableValueException when type is not known or not in body in discriminator map (lyrixx) + * feature #43118 [FrameworkBundle] Remove deprecated code (IonBazan) + * feature #43121 [Notifier] [GoogleChat] remove support for deprecated "threadKey" parameter (IonBazan) + * feature #42338 [DomCrawler] Added Crawler::innerText() method (Bilge) + * feature #43095 [Form] Add the EnumType (derrabus) + * feature #43094 [Console] Add support of RGB functional notation (alexandre-daubois) + * feature #43098 [Validator] Add error's uid to `Count` and `Length` constraints with "exactly" option enabled (VladGapanovich) + * feature #42668 [Yaml] Use more concise float representation in dump (dev97) + * feature #43017 [HttpFoundation] Map `multipart/form-data` as `form` Content-Type (keichinger) + * feature #43015 [DependencyInjection] Allow injecting tagged iterator as service locator arguments (IonBazan) + * feature #42991 [FrameworkBundle] Add configureContainer(), configureRoutes() and getConfigDir() to MicroKernelTrait (nicolas-grekas) + * feature #43018 [Mailer] Adding support for TagHeader and MetadataHeader to the Sendgrid API transport (gnito-org) + * feature #43010 Remove remaining support for Doctrine Cache (derrabus) + * feature #42988 [ErrorHandler] Add helper script to patch type declarations (wouterj) + * feature #42982 Add Session Token to Amazon Mailer (Jubeki) + * feature #42959 [DependencyInjection] Make auto-aliases private by default (nicolas-grekas) + * feature #42957 [RateLimiter][Runtime][Translation] remove ``@experimental`` flag (nicolas-grekas) + * feature #41163 [Mesenger] Add support for reseting container services between 2 messages (lyrixx) + * feature #42967 [Cache] Remove support for Doctrine Cache (derrabus) + * feature #41858 [Translation] Translate translatable parameters (kylekatarnls) + * feature #42941 Implement Message Stream for Postmark Mailer (driesvints) + * feature #42532 [DependencyInjection] Sort services in service locator according to priority (BoShurik) + * feature #42502 [Serializer] Add support for collecting type error during denormalization (lyrixx) + * feature #40120 [Cache] Add CouchbaseCollectionAdapter compatibility with sdk 3.0.0 (ajcerezo) + * feature #42965 [Cache] Deprecate support for Doctrine Cache (derrabus) + * feature #41615 [Serializer] Add option to skip uninitialized typed properties (vuryss) + * feature #41566 [FrameworkBundle] Introduced new method for getting bundles config path (a-menshchikov) + * feature #42925 [DoctrineBridge] Remove DoctrineTestHelper and TestRepositoryFactory (derrabus) + * feature #42881 [Console] Add more context when CommandIsSuccessful fails (yoannrenard) + * feature #41321 [FrameworkBundle] Remove deprecate session service (jderusse) + * feature #42900 [HttpFoundation] Add a flag to hasSession to distinguished session from factory (jderusse) + * feature #41390 [HttpKernel] Add session cookie handling in cli context (alexander-schranz, Nyholm) + * feature #42800 Display the roles of the logged-in user in the Web Debug Toolbar (NicoHaase) + * feature #42872 [Mime] Update mime types (fabpot) + * feature #42039 [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters (ruudk) + * feature #42710 [Mailer] Added OhMySMTP Bridge (paul-oms) + * feature #40987 [Config] Handle ignoreExtraKeys in config builder (HypeMC) + * feature #42426 [Notifier] Autoconfigure chatter.transport_factory (ismail1432) + * feature #42748 [Notifier] Add Esendex message ID to SentMessage object (benr77) + * feature #42526 [FrameworkBundle] Add BrowserKitAssertionsTrait::assertThatForBrowser (koenreiniers) + * feature #41527 [Ldap] Fixing the behaviour of getting LDAP Attributes (mr-sven) + * feature #42623 [ErrorHandler] Turn return-type annotations into deprecations by default + add mode to turn them into native types (nicolas-grekas) + * feature #42695 [Mailer] Restore Transport signatures (derrabus) + * feature #42698 Notifier final transport (fabpot) + * feature #42696 [Notifier] Mark Transport as final (fabpot) + * feature #42433 [Notifier] Add more explicit error if a SMSChannel doesn't have a Recipient (ismail1432) + * feature #42619 [Serializer] Deprecate support for returning empty, iterable, countable, raw object when normalizing (lyrixx) + * feature #42662 [Mailer] Consume a PSR-14 event dispatcher (derrabus) + * feature #42625 [DependencyInjection] Add service_closure() to the PHP-DSL (HypeMC) + * feature #42650 [Security] make TokenInterface::getUser() nullable to tell about unauthenticated tokens (nicolas-grekas) + * feature #42644 [Security] Make `AuthenticationTrustResolverInterface::isAuthenticated()` non-virtual (chalasr) + * feature #42634 [Console] Remove `HelperSet::setCommand()` and `getCommand()` (derrabus) + * feature #42632 [Console] Deprecate `HelperSet::setCommand()` and `getCommand()` (derrabus) + * feature #41994 [Validator] Add support of nested attributes (alexandre-daubois) + * feature #41613 [Security] Remove everything related to the deprecated authentication manager (wouterj) + * feature #42595 Fix incompatibilities with upcoming security 6.0 (wouterj) + * feature #42578 [Security] Deprecate legacy remember me services (wouterj) + * feature #42516 [Security] Deprecate built-in authentication entry points (wouterj) + * feature #42387 [Form] Deprecate calling FormErrorIterator::children() if the current element is not iterable (W0rma) + * feature #39641 [Yaml] Add --exclude and negatable --parse-tags option to lint:yaml command (christingruber) + * feature #42510 [Security] Deprecate remaining anonymous checks (wouterj) + * feature #42423 [Security] Deprecate AnonymousToken, non-UserInterface users, and token credentials (wouterj) + * feature #41954 [Filesystem] Add the Path class (theofidry) + * feature #42442 [FrameworkBundle] Deprecate AbstractController::get() and has() (fabpot) + * feature #42422 Clarify goals of AbstractController (fabpot) + * feature #42420 [Security] Deprecate legacy signatures (wouterj) + * feature #41754 [SecurityBundle] Create a smooth upgrade path for security factories (wouterj) + * feature #42198 [Security] Deprecate `PassportInterface` (chalasr) + * feature #42332 [HttpFoundation] Add `litespeed_finish_request` to `Response` (thomas2411) + * feature #42286 [HttpFoundation] Add `SessionFactoryInterface` (kbond) + * feature #42392 [HttpFoundation] Mark Request::get() internal (ro0NL) + * feature #39601 [Notifier] add `SentMessageEvent` and `FailedMessageEvent` (ismail1432) + * feature #42188 [Notifier] Add FakeChat Logger transport (noniagriconomie) + * feature #41522 [Notifier] Add TurboSms Bridge (fre5h) + * feature #42337 [Validator] Remove internal from `ConstraintViolationAssertion` (jordisala1991) + * feature #42333 [Security] Remove deprecated logout handlers (chalasr) + * feature #42123 [Notifier] Add FakeSMS Logger transport (noniagriconomie) + * feature #42297 [Serializer] Add support for serializing empty array as object (lyrixx) + * feature #42326 [Security] Deprecate remaining `LogoutHandlerInterface` implementations (chalasr) + * feature #42219 [Mailer] Add support of ping_threshold to SesTransportFactory (Tyraelqp) + * feature #40052 [ErrorHandler] Add button to copy the path where error is thrown (lmillucci) + * feature #38495 [Asset] [DX] Option to make asset manifests strict on missing item (GromNaN) + * feature #39828 [Translation] XliffLintCommand supports Github Actions annotations (YaFou) + * feature #39826 [TwigBridge] LintCommand supports Github Actions annotations (YaFou) + * feature #39141 [Notifier] Add Amazon SNS bridge (adrien-chinour) + * feature #42240 [Serializer] Add support for preserving empty object in object property (lyrixx) + * feature #42239 [Notifier] Add Yunpian Notifier Bridge (welcoMattic) + * feature #42195 [WebProfilerBundle] Redesigned the log section (javiereguiluz) + * feature #42176 [Console][HttpKernel] Implement `psr/log` 3 (derrabus) + * feature #42163 [Messenger] [Redis] Prepare turning `delete_after_ack` to `true` in 6.0 (chalasr) + * feature #42180 [Notifier] Add bridge for smsc.ru (kozlice) + * feature #42172 [Finder] Remove deprecated code (derrabus) + * feature #42137 [Finder] Make Comparator immutable (derrabus) + * feature #42142 [Security] Remove CSRF deprecations (derrabus) + * feature #42133 [FrameworkBundle] Remove deprecated options in translation:update command (javiereguiluz) + * feature #42127 [ExpressionLanguage] Store compiler and evaluator as closures (derrabus) + * feature #42088 [Contracts] add return types and bump to v3 (nicolas-grekas) + * feature #42094 [Notifier] [Slack] Throw error if maximum block limit is reached for slack message options (norkunas) + * feature #42050 [Security] Deprecate `TokenInterface::isAuthenticated()` (chalasr) + * feature #42090 [Notifier] [Slack] Include additional errors to slack notifier error message (norkunas) + * feature #41319 [Messenger] Removed deprecated code (Nyholm) + * feature #41982 [Security] Remove getPassword() and getSalt() from UserInterface (chalasr) + * feature #41989 [Cache] make `LockRegistry` use semaphores when possible (nicolas-grekas) + * feature #41965 [Security] Deprecate "always authenticate" and "exception on no token" (wouterj) + * feature #41290 [Cache] Implement psr/cache 3 (derrabus) + * feature #41962 add ability to style doubles and integers independently (1ma) + * feature #40830 [Serializer] Add support of PHP backed enumerations (alexandre-daubois) + * feature #41976 [Cache] Remove DoctrineProvider (derrabus) + * feature #40908 [Cache] Deprecate DoctrineProvider (derrabus) + * feature #41717 Allow TranslatableMessage object in form option 'help' (scuben) + * feature #41963 [HttpKernel] remove deprecated features (nicolas-grekas) + * feature #41960 [PasswordHasher][Security] Remove legacy password encoders (chalasr) + * feature #41705 [Notifier] add Mailjet SMS bridge (jnadaud) + * feature #41657 [Serializer] Remove deprecation layer (derrabus) + * feature #41937 [EventDispatcher] Remove ability to configure tags on RegisterListenersPass (derrabus) + * feature #41932 [DependencyInjection] Remove deprecated code (derrabus) + * feature #41851 Add TesterTrait::assertCommandIsSuccessful() helper (yoannrenard) + * feature #39623 [Messenger] Added StopWorkerException (lyrixx) + * feature #41292 [Workflow] Add support for getting updated context after a transition (lyrixx) + * feature #41154 [Validator] Add support for `ConstraintViolationList::createFromMessage()` (lyrixx) + * feature #41874 [SecurityBundle] Hide security toolbar if no firewall matched (wouterj) + * feature #41375 [Notifier] Add MessageMedia Bridge (vuphuong87) + * feature #41923 [EventDispatcher] Deprecate configuring tags on RegisterListenersPass (derrabus) + * feature #41802 [Uid] Add NilUlid (fancyweb) + * feature #40738 [Notifier] Add options to Microsoft Teams notifier (OskarStark) + * feature #41172 [Notifier] Add Telnyx notifier bridge (StaffNowa) + * feature #41770 [HttpClient] Add default base_uri to MockHttpClient (nicolas-grekas) + * feature #41205 [TwigBridge] Add `encore_entry_*_tags()` to UndefinedCallableHandler, as no-op (nicolas-grekas) + * feature #41786 [FrameworkBundle] Add commented base64 version of secrets' keys (nicolas-grekas) + * feature #41432 [WebProfilerBundle] Improved the light/dark theme switching (javiereguiluz) + * feature #41743 [Form] remove remaining deprecation layers (xabbuh) + * feature #41692 [Form] remove deprecated constants (xabbuh) + * feature #41540 [VarDumper] Add casters for Symfony UUIDs and ULIDs (fancyweb) + * feature #41530 [FrameworkBundle] Deprecate the public `profiler` service to private (nicolas-grekas) + * feature #41392 [Validator] Remove deprecated code (jschaedl) + * feature #41318 [Form] Remove deprecated code (yceruto) + * feature #41308 [Mailer] Remove deprecated code (jderusse) + * feature #41299 Remove Serializable implementations (derrabus) + * feature #41350 [Inflector] Remove the component (fancyweb) + * feature #41361 [Intl] Removed deprecated code (malteschlueter) + * feature #41365 [PropertyAccess] Remove deprecated code (malteschlueter) + * feature #41371 [Routing] Remove deprecation layer (derrabus) + * feature #41199 [FrameworkBundle] Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead (nicolas-grekas) + * feature #41304 [EventDispatcher] Remove LegacyEventDispatcherProxy (derrabus) + * feature #41302 [PhpUnitBridge] Remove SetUpTearDownTrait (derrabus) + * feature #41363 [Ldap] Removed deprecated code (malteschlueter) + * feature #41364 [Mime] Remove deprecated code (malteschlueter) + * feature #41359 [HttpClient] Removed deprecated code (malteschlueter) + * feature #41360 [Yaml] Remove deprecated code (fancyweb) + * feature #41358 [EventDispatcher] Removed deprecated code (malteschlueter) + * feature #41357 [Dotenv] Remove deprecated code (malteschlueter) + * feature #41355 [DomCrawler] Removed deprecated code (malteschlueter) + * feature #41353 [Cache] Removed depreacted code (malteschlueter) + * feature #41351 [FrameworkBundle][SecurityBundle][TwigBundle] Turn deprecated public services to private (fancyweb) + * feature #41334 [HttpFoundation] remove deprecated code (azjezz) + * feature #41316 [OptionsResolver] Remove deprecated code (yceruto) + * feature #41314 [Messenger] Remove dependency on bridge packages (Nyholm) + * feature #41284 [Lock] Remove deprecated classes in Lock (jderusse) + * feature #41312 [Console] Remove console deprecations (jschaedl) + * feature #41303 [Config] Remove deprecated code (derrabus) + * feature #41301 [MonologBridge] Remove deprecated code (derrabus) + * feature #41300 [Asset] Remove deprecated RemoteJsonManifestVersionStrategy (mbabker) + * feature #41298 [Notifier] Remove deprecation in slack-notifier (jschaedl) + * feature #41203 [FrameworkBundle] Add autowiring alias for `HttpCache\StoreInterface` (nicolas-grekas) + * feature #41282 Bump Symfony 6 to PHP 8 (nicolas-grekas) + diff --git a/README.md b/README.md index bddcd21f97762..fb519d5f0e2ab 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@

-[Symfony][1] is a **PHP framework** for web and console applications and a set of reusable -**PHP components**. Symfony is used by thousands of web applications (including -BlaBlaCar.com and Spotify.com) and most of the [popular PHP projects][2] (including -Drupal and Magento). +[Symfony][1] is a **PHP framework** for web and console applications and a set +of reusable **PHP components**. Symfony is used by thousands of web +applications and most of the [popular PHP projects][2]. Installation ------------ @@ -15,11 +14,24 @@ Installation Support" (LTS) versions and has a [release process][6] that is predictable and business-friendly. +Sponsor +------- + +Symfony 6.0 is [backed][27] by [SensioLabs][28]. + +As the creator of Symfony, SensioLabs supports companies using Symfony, with an +offering encompassing consultancy, expertise, services, training, and technical +assistance to ensure the success of web application development projects. + +Help Symfony by [sponsoring][29] its development! + + Documentation ------------- * Read the [Getting Started guide][7] if you are new to Symfony. * Try the [Symfony Demo application][23] to learn Symfony in practice. +* Discover Symfony ecosystem in detail with [Symfony The Fast Track][26]. * Master Symfony with the [Guides and Tutorials][8], the [Components docs][9] and the [Best Practices][10] reference. @@ -46,8 +58,8 @@ If you discover a security vulnerability within Symfony, please follow our About Us -------- -Symfony development is sponsored by [SensioLabs][21], led by the -[Symfony Core Team][22] and supported by [Symfony contributors][19]. +Symfony development is led by the [Symfony Core Team][22] +and supported by [Symfony contributors][19]. [1]: https://symfony.com [2]: https://symfony.com/projects @@ -69,8 +81,11 @@ Symfony development is sponsored by [SensioLabs][21], led by the [18]: https://symfony.com/doc/current/contributing/documentation/index.html [19]: https://symfony.com/contributors [20]: https://symfony.com/security -[21]: https://sensiolabs.com [22]: https://symfony.com/doc/current/contributing/code/core_team.html [23]: https://github.com/symfony/symfony-demo [24]: https://symfony.com/coc [25]: https://symfony.com/doc/current/contributing/code_of_conduct/care_team.html +[26]: https://symfony.com/book +[27]: https://symfony.com/backers +[28]: https://sensiolabs.com/ +[29]: https://symfony.com/sponsor diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md deleted file mode 100644 index a7e49d469c502..0000000000000 --- a/UPGRADE-4.0.md +++ /dev/null @@ -1,1143 +0,0 @@ -UPGRADE FROM 3.x to 4.0 -======================= - -Symfony Framework ------------------ - -The first step to upgrade a Symfony 3.x application to 4.x is to update the -file and directory structure of your application: - -| Symfony 3.x | Symfony 4.x -| ----------------------------------- | -------------------------------- -| `app/config/` | `config/` -| `app/config/*.yml` | `config/*.yaml` and `config/packages/*.yaml` -| `app/config/parameters.yml.dist` | `config/services.yaml` and `.env.dist` -| `app/config/parameters.yml` | `config/services.yaml` and `.env` -| `app/Resources//views/` | `templates/bundles//` -| `app/Resources/` | `src/Resources/` -| `app/Resources/assets/` | `assets/` -| `app/Resources/translations/` | `translations/` -| `app/Resources/views/` | `templates/` -| `src/AppBundle/` | `src/` -| `var/logs/` | `var/log/` -| `web/` | `public/` -| `web/app.php` | `public/index.php` -| `web/app_dev.php` | `public/index.php` - -Then, upgrade the contents of your console script and your front controller: - -* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/4.4/bin/console -* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.4/public/index.php - -Lastly, read the following article to add Symfony Flex to your application and -upgrade the configuration files: https://symfony.com/doc/current/setup/flex.html - -If you use Symfony components instead of the whole framework, you can find below -the upgrading instructions for each individual bundle and component. - -ClassLoader ------------ - - * The component has been removed. Use Composer instead. - -Config ------- - - * The protected `TreeBuilder::$builder` property has been removed. - -Console -------- - - * Setting unknown style options is not supported anymore and throws an - exception. - - * The `QuestionHelper::setInputStream()` method is removed. Use - `StreamableInputInterface::setStream()` or `CommandTester::setInputs()` - instead. - - Before: - - ```php - $input = new ArrayInput(); - - $questionHelper->setInputStream($stream); - $questionHelper->ask($input, $output, $question); - ``` - - After: - - ```php - $input = new ArrayInput(); - $input->setStream($stream); - - $questionHelper->ask($input, $output, $question); - ``` - - Before: - - ```php - $commandTester = new CommandTester($command); - - $stream = fopen('php://memory', 'r+', false); - fputs($stream, "AppBundle\nYes"); - rewind($stream); - - $command->getHelper('question')->setInputStream($stream); - - $commandTester->execute(); - ``` - - After: - - ```php - $commandTester = new CommandTester($command); - - $commandTester->setInputs(['AppBundle', 'Yes']); - - $commandTester->execute(); - ``` - - * The `console.exception` event and the related `ConsoleExceptionEvent` class have - been removed in favor of the `console.error` event and the `ConsoleErrorEvent` class. - - * The `SymfonyQuestionHelper::ask` default validation has been removed in favor of `Question::setValidator`. - -Debug ------ - - - * The `ContextErrorException` class has been removed. Use `\ErrorException` instead. - - * `FlattenException::getTrace()` now returns additional type descriptions - `integer` and `float`. - - * Support for stacked errors in the `ErrorHandler` has been removed - -DependencyInjection -------------------- - - * Definitions and aliases are now private by default in 4.0. You should either use service injection - or explicitly define your services as public if you really need to inject the container. - - * Relying on service auto-registration while autowiring is not supported anymore. - Explicitly inject your dependencies or create services whose ids are - their fully-qualified class name. - - Before: - - ```php - namespace App\Controller; - - use App\Mailer; - - class DefaultController - { - public function __construct(Mailer $mailer) { - // ... - } - - // ... - } - ``` - ```yml - services: - App\Controller\DefaultController: - autowire: true - ``` - - After: - - ```php - // same PHP code - ``` - ```yml - services: - App\Controller\DefaultController: - autowire: true - - # or - # App\Controller\DefaultController: - # arguments: { $mailer: "@App\Mailer" } - - App\Mailer: - autowire: true - ``` - - * Autowiring services based on the types they implement is not supported anymore. - It will only look for an alias or a service id that matches a given FQCN. - Rename (or alias) your services to their FQCN id to make them autowirable. - In 3.4, you can activate this behavior instead of having deprecation messages - by setting the following parameter: - - ```yml - parameters: - container.autowiring.strict_mode: true - ``` - - From 4.0, you can remove it as it's the default behavior and the parameter is not handled anymore. - - * `_defaults` and `_instanceof` are now reserved service names in Yaml configurations. Please rename any services with that names. - - * Non-numeric keys in methods and constructors arguments have never been supported and are now forbidden. Please remove them if you happen to have one. - - * Service names that start with an underscore are now reserved in Yaml files. Please rename any services with such names. - - * Autowiring-types have been removed, use aliases instead. - - Before: - - ```xml - - Doctrine\Common\Annotations\Reader - - ``` - - After: - - ```xml - - - ``` - - * Service identifiers and parameter names are now case sensitive. - - * The `Reference` and `Alias` classes do not make service identifiers lowercase anymore. - - * Using the `PhpDumper` with an uncompiled `ContainerBuilder` is not supported - anymore. - - * Extending the containers generated by `PhpDumper` is not supported - anymore. - - * The `DefinitionDecorator` class has been removed. Use the `ChildDefinition` - class instead. - - * The `ResolveDefinitionTemplatesPass` class has been removed. - Use the `ResolveChildDefinitionsPass` class instead. - - * Using unsupported configuration keys in YAML configuration files raises an - exception. - - * Using unsupported options to configure service aliases raises an exception. - - * Setting or unsetting a service with the `Container::set()` method is - no longer supported. Only synthetic services can be set or unset. - - * Checking the existence of a private service with the `Container::has()` - method is no longer supported and will return `false`. - - * Requesting a private service with the `Container::get()` method is no longer - supported. - - * The ``strict`` attribute in service arguments has been removed. - The attribute is ignored since 3.0, you can remove it. - - * Top-level anonymous services in XML are no longer supported. - - * The `ExtensionCompilerPass` has been moved to before-optimization passes with priority -1000. - - * In 3.4, parameter `container.dumper.inline_class_loader` was introduced. Unless - you're using a custom autoloader, you should enable this parameter. This can - drastically improve DX by reducing the time to load classes when the `DebugClassLoader` - is enabled. If you're using `FrameworkBundle`, this performance improvement will - also impact the "dev" environment: - - ```yml - parameters: - container.dumper.inline_class_loader: true - ``` - -DoctrineBridge --------------- - - * The `Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandler` and - `Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandlerSchema` have been removed. Use - `Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler` instead. - -EventDispatcher ---------------- - - * The `ContainerAwareEventDispatcher` class has been removed. - Use `EventDispatcher` with closure factories instead. - - * The `reset()` method has been added to `TraceableEventDispatcherInterface`. - -ExpressionLanguage ------------------- - - * The ability to pass a `ParserCacheInterface` instance to the `ExpressionLanguage` - class has been removed. You should use the `CacheItemPoolInterface` interface - instead. - -Filesystem ----------- - - * The `Symfony\Component\Filesystem\LockHandler` has been removed, - use the `Symfony\Component\Lock\Store\FlockStore` class - or the `Symfony\Component\Lock\Store\FlockStore\SemaphoreStore` class directly instead. - * Support for passing relative paths to `Filesystem::makePathRelative()` has been removed. - -Finder ------- - - * The `ExceptionInterface` has been removed. - * The `Symfony\Component\Finder\Iterator\FilterIterator` class has been - removed as it used to fix a bug which existed before version 5.5.23/5.6.7 - -Form ----- - - * The values of the `FormEvents::*` constants have been updated to match the - constant names. You should only update your application if you relied on the - constant values instead of their names. - - * The `choices_as_values` option of the `ChoiceType` has been removed. - - * Support for data objects that implements both `Traversable` and - `ArrayAccess` in `ResizeFormListener::preSubmit` method has been removed. - - * Using callable strings as choice options in ChoiceType is not supported - anymore. - - Before: - - ```php - 'choice_label' => 'strtoupper', - ``` - - After: - - ```php - 'choice_label' => function ($choice) { - return strtoupper($choice); - }, - ``` - - * Caching of the loaded `ChoiceListInterface` in the `LazyChoiceList` has been removed, - it must be cached in the `ChoiceLoaderInterface` implementation instead. - - * Calling `isValid()` on a `Form` instance before submitting it is not supported - anymore and raises an exception. - - Before: - - ```php - if ($form->isValid()) { - // ... - } - ``` - - After: - - ```php - if ($form->isSubmitted() && $form->isValid()) { - // ... - } - ``` - - * Using the "choices" option in ``CountryType``, ``CurrencyType``, ``LanguageType``, - ``LocaleType``, and ``TimezoneType`` without overriding the ``choice_loader`` - option is now ignored. - - Before: - ```php - $builder->add('custom_locales', LocaleType::class, [ - 'choices' => $availableLocales, - ]); - ``` - - After: - ```php - $builder->add('custom_locales', LocaleType::class, [ - 'choices' => $availableLocales, - 'choice_loader' => null, - ]); - // or - $builder->add('custom_locales', LocaleType::class, [ - 'choice_loader' => new CallbackChoiceLoader(function () { - return $this->getAvailableLocales(); - }), - ]); - ``` - - * Removed `ChoiceLoaderInterface` implementation in `TimezoneType`. Use the "choice_loader" option instead. - - Before: - ```php - class MyTimezoneType extends TimezoneType - { - public function loadChoiceList() - { - // override the method - } - } - ``` - - After: - ```php - class MyTimezoneType extends AbstractType - { - public function getParent() - { - return TimezoneType::class; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefault('choice_loader', ...); // override the option instead - } - } - ``` - - * `FormRendererInterface::setTheme` and `FormRendererEngineInterface::setTheme` have a new optional argument `$useDefaultThemes` with a default value set to `true`. - -FrameworkBundle ---------------- - - * The `session.use_strict_mode` option has been removed and strict mode is always enabled. - - * The `validator.mapping.cache.doctrine.apc` service has been removed. - - * The "framework.trusted_proxies" configuration option and the corresponding "kernel.trusted_proxies" parameter have been removed. Use the `Request::setTrustedProxies()` method in your front controller instead. - - * The default value of the `framework.workflows.[name].type` configuration options is now `state_machine`. - - * Support for absolute template paths has been removed. - - * The following form types registered as services have been removed; use their - fully-qualified class name instead: - - - `"form.type.birthday"` - - `"form.type.checkbox"` - - `"form.type.collection"` - - `"form.type.country"` - - `"form.type.currency"` - - `"form.type.date"` - - `"form.type.datetime"` - - `"form.type.email"` - - `"form.type.file"` - - `"form.type.hidden"` - - `"form.type.integer"` - - `"form.type.language"` - - `"form.type.locale"` - - `"form.type.money"` - - `"form.type.number"` - - `"form.type.password"` - - `"form.type.percent"` - - `"form.type.radio"` - - `"form.type.range"` - - `"form.type.repeated"` - - `"form.type.search"` - - `"form.type.textarea"` - - `"form.type.text"` - - `"form.type.time"` - - `"form.type.timezone"` - - `"form.type.url"` - - `"form.type.button"` - - `"form.type.submit"` - - `"form.type.reset"` - - * The `framework.serializer.cache` option and the services - `serializer.mapping.cache.apc` and `serializer.mapping.cache.doctrine.apc` - have been removed. APCu should now be automatically used when available. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CompilerDebugDumpPass` has been removed. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass` has been removed. - Use `Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass` instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass` class has been removed. - Use the `Symfony\Component\Serializer\DependencyInjection\SerializerPass` class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass` class has been - removed. Use the `Symfony\Component\Form\DependencyInjection\FormPass` class instead. - - * The `Symfony\Bundle\FrameworkBundle\EventListener\SessionListener` class has been removed. - Use the `Symfony\Component\HttpKernel\EventListener\SessionListener` class instead. - - * The `Symfony\Bundle\FrameworkBundle\EventListener\TestSessionListener` class has been - removed. Use the `Symfony\Component\HttpKernel\EventListener\TestSessionListener` - class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass` class has been removed. - Use tagged iterator arguments instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass` class has been - removed. Use the `Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass` - class instead. - - * Class parameters related to routing have been removed - * router.options.generator_class - * router.options.generator_base_class - * router.options.generator_dumper_class - * router.options.matcher_class - * router.options.matcher_base_class - * router.options.matcher_dumper_class - * router.options.matcher.cache_class - * router.options.generator.cache_class - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ControllerArgumentValueResolverPass` class - has been removed. Use the `Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass` - class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RoutingResolverPass` - class has been removed. Use the - `Symfony\Component\Routing\DependencyInjection\RoutingResolverPass` class instead. - - * The `Symfony\Bundle\FrameworkBundle\Translation\Translator` constructor now takes the - default locale as mandatory 3rd argument. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass` class has been - removed. Use the `Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass` - class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass` class has been - removed. Use the `Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass` - class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ValidateWorkflowsPass` class - has been removed. Use the `Symfony\Component\Workflow\DependencyInjection\ValidateWorkflowsPass` - class instead. - - * Using the `KERNEL_DIR` environment variable and the automatic guessing based - on the `phpunit.xml` file location have been removed from the `KernelTestCase::getKernelClass()` - method implementation. Set the `KERNEL_CLASS` environment variable to the - fully-qualified class name of your Kernel or override the `KernelTestCase::createKernel()` - or `KernelTestCase::getKernelClass()` method instead. - - * The methods `KernelTestCase::getPhpUnitXmlDir()` and `KernelTestCase::getPhpUnitCliConfigArgument()` - have been removed. - - * The `Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory` class has been removed. - Use `Symfony\Component\Validator\ContainerConstraintValidatorFactory` instead. - - * The `--no-prefix` option of the `translation:update` command has - been removed. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheClearerPass` class has been removed. - Use tagged iterator arguments instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddCacheWarmerPass` class has been removed. - Use tagged iterator arguments instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass` - class has been removed. Use the - `Symfony\Component\Translation\DependencyInjection\TranslationDumperPass` class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass` - class has been removed. Use the - `Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass` class instead. - - * The `Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslatorPass` - class has been removed. Use the - `Symfony\Component\Translation\DependencyInjection\TranslatorPass` class instead. - - * The `Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader` - class has been deprecated and will be removed in 4.0. Use the - `Symfony\Component\Translation\Reader\TranslationReader` class instead. - - * The `translation.loader` service has been removed. - Use the `translation.reader` service instead. - - * `AssetsInstallCommand::__construct()` now requires an instance of - `Symfony\Component\Filesystem\Filesystem` as first argument. - - * `CacheClearCommand::__construct()` now requires an instance of - `Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface` as - first argument. - - * `CachePoolClearCommand::__construct()` now requires an instance of - `Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer` as - first argument. - - * `EventDispatcherDebugCommand::__construct()` now requires an instance of - `Symfony\Component\EventDispatcher\EventDispatcherInterface` as - first argument. - - * `RouterDebugCommand::__construct()` now requires an instance of - `Symfony\Component\Routing\RouterInterface` as - first argument. - - * `RouterMatchCommand::__construct()` now requires an instance of - `Symfony\Component\Routing\RouterInterface` as - first argument. - - * `TranslationDebugCommand::__construct()` now requires an instance of - `Symfony\Component\Translation\TranslatorInterface` as - first argument. - - * `TranslationUpdateCommand::__construct()` now requires an instance of - `Symfony\Component\Translation\TranslatorInterface` as - first argument. - - * The `Symfony\Bundle\FrameworkBundle\Translation\PhpExtractor` - class has been deprecated and will be removed in 4.0. Use the - `Symfony\Component\Translation\Extractor\PhpExtractor` class instead. - - * The `Symfony\Bundle\FrameworkBundle\Translation\PhpStringTokenParser` - class has been deprecated and will be removed in 4.0. Use the - `Symfony\Component\Translation\Extractor\PhpStringTokenParser` class instead. - -HttpFoundation --------------- - - * The `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument. - See http://symfony.com/doc/current/components/http_foundation/trusting_proxies.html for more info. - - * The `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods have been removed. - - * Extending the following methods of `Response` - is no longer possible (these methods are now `final`): - - - `setDate`/`getDate` - - `setExpires`/`getExpires` - - `setLastModified`/`getLastModified` - - `setProtocolVersion`/`getProtocolVersion` - - `setStatusCode`/`getStatusCode` - - `setCharset`/`getCharset` - - `setPrivate`/`setPublic` - - `getAge` - - `getMaxAge`/`setMaxAge` - - `setSharedMaxAge` - - `getTtl`/`setTtl` - - `setClientTtl` - - `getEtag`/`setEtag` - - `hasVary`/`getVary`/`setVary` - - `isInvalid`/`isSuccessful`/`isRedirection`/`isClientError`/`isServerError` - - `isOk`/`isForbidden`/`isNotFound`/`isRedirect`/`isEmpty` - - * The ability to check only for cacheable HTTP methods using `Request::isMethodSafe()` is - not supported anymore, use `Request::isMethodCacheable()` instead. - - * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler` class has been - removed. Implement `SessionUpdateTimestampHandlerInterface` or extend `AbstractSessionHandler` instead. - - * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler` and - `Symfony\Component\HttpFoundation\Session\Storage\Proxy\NativeProxy` classes have been removed. - - * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler` does not work with the legacy - mongo extension anymore. It requires mongodb/mongodb package and ext-mongodb. - - * The `Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcacheSessionHandler` class has been removed. - Use `Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler` instead. - -HttpKernel ----------- - - * Bundle inheritance has been removed. - - * Relying on convention-based commands discovery is not supported anymore. - Use PSR-4 based service discovery instead. - - Before: - - ```yml - # app/config/services.yml - services: - # ... - - # implicit registration of all commands in the `Command` folder - ``` - - After: - - ```yml - # app/config/services.yml - services: - # ... - - # explicit commands registration - AppBundle\Command\: - resource: '../../src/AppBundle/Command/*' - tags: ['console.command'] - ``` - - * The `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()` methods have been removed. - - * Possibility to pass non-scalar values as URI attributes to the ESI and SSI - renderers has been removed. The inline fragment renderer should be used with - non-scalar attributes. - - * The `ControllerResolver::getArguments()` method has been removed. If you - have your own `ControllerResolverInterface` implementation, you should - inject an `ArgumentResolverInterface` instance. - - * The `DataCollector::varToString()` method has been removed in favor of `cloneVar()`. - - * The `Psr6CacheClearer::addPool()` method has been removed. Pass an array of pools indexed - by name to the constructor instead. - - * The `LazyLoadingFragmentHandler::addRendererService()` method has been removed. - - * The `X-Status-Code` header method of setting a custom status code in the - response when handling exceptions has been removed. There is now a new - `GetResponseForExceptionEvent::allowCustomResponseCode()` method instead, - which will tell the Kernel to use the response code set on the event's - response object. - - * The `Kernel::getEnvParameters()` method has been removed. - - * The `SYMFONY__` environment variables are no longer processed automatically - by Symfony. Use the `%env()%` syntax to get the value of any environment - variable from configuration files instead. - - * The `getCacheDir()` method of your kernel should not be called while building the container. - Use the `%kernel.cache_dir%` parameter instead. Not doing so may break the `cache:clear` command. - - * The `Symfony\Component\HttpKernel\Config\EnvParametersResource` class has been removed. - - * The `reset()` method has been added to `Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface`. - - * The `clear()` method has been added to `Symfony\Component\HttpKernel\Log\DebugLoggerInterface`. - - * The `ChainCacheClearer::add()` method has been removed, - inject the list of clearers as a constructor argument instead. - - * The `CacheWarmerAggregate::add()` and `setWarmers()` methods have been removed, - inject the list of clearers as a constructor argument instead. - - * The `CacheWarmerAggregate` and `ChainCacheClearer` classes have been made final. - -Ldap ----- - - * The `RenameEntryInterface` has been removed, and merged with `EntryManagerInterface` - -Process -------- - - * Passing a not existing working directory to the constructor of the `Symfony\Component\Process\Process` class is not supported anymore. - - * The `Symfony\Component\Process\ProcessBuilder` class has been removed, - use the `Symfony\Component\Process\Process` class directly instead. - - * The `ProcessUtils::escapeArgument()` method has been removed, use a command line array or give env vars to the `Process::start/run()` method instead. - - * Environment variables are always inherited in sub-processes. - - * Configuring `proc_open()` options has been removed. - - * Configuring Windows and sigchild compatibility is not possible anymore - they are always enabled. - - * Extending `Process::run()`, `Process::mustRun()` and `Process::restart()` is - not supported anymore. - - * The `getEnhanceWindowsCompatibility()` and `setEnhanceWindowsCompatibility()` methods of the `Process` class have been removed. - -Profiler --------- - - * The `profiler.matcher` option has been removed. - -ProxyManager ------------- - - * The `ProxyDumper` class has been made final - -Security --------- - - * The `RoleInterface` has been removed. Extend the `Symfony\Component\Security\Core\Role\Role` - class instead. - - * The `LogoutUrlGenerator::registerListener()` method expects a 6th `string $context = null` argument. - - * The `AccessDecisionManager::setVoters()` method has been removed. Pass the - voters to the constructor instead. - - * Support for defining voters that don't implement the `VoterInterface` has been removed. - - * Calling `ContextListener::setLogoutOnUserChange(false)` won't have any - effect anymore. - - * Removed the HTTP digest authentication system. The `NonceExpiredException`, - `DigestAuthenticationListener` and `DigestAuthenticationEntryPoint` classes - have been removed. Use another authentication system like `http_basic` instead. - - * The `GuardAuthenticatorInterface` interface has been removed. - Use `AuthenticatorInterface` instead. - - * When extending `AbstractGuardAuthenticator` getCredentials() cannot return - `null` anymore, return false from `supports()` if no credentials available instead. - -SecurityBundle --------------- - - * The `FirewallContext::getContext()` method has been removed, use the `getListeners()` and/or `getExceptionListener()` method instead. - - * The `FirewallMap::$map` and `$container` properties have been removed. - - * The `UserPasswordEncoderCommand` class does not allow `null` as the first argument anymore. - - * `UserPasswordEncoderCommand` does not extend `ContainerAwareCommand` nor implement `ContainerAwareInterface` anymore. - - * `InitAclCommand` has been removed. Use `Symfony\Bundle\AclBundle\Command\InitAclCommand` instead - - * `SetAclCommand` has been removed. Use `Symfony\Bundle\AclBundle\Command\SetAclCommand` instead - - * The firewall option `logout_on_user_change` is now always true, which will - trigger a logout if the user changes between requests. - - * Removed the HTTP digest authentication system. The `HttpDigestFactory` class - has been removed. Use another authentication system like `http_basic` instead. - - * The `switch_user.stateless` option is now always true if the firewall is stateless. - - * Not configuring explicitly the provider on a firewall is ambiguous when there is more than one registered provider. - The first configured provider is not used anymore and an exception is thrown instead. - Explicitly configure the provider to use on your firewalls. - -Serializer ----------- - - * The ability to pass a Doctrine `Cache` instance to the `ClassMetadataFactory` - class has been removed. You should use the `CacheClassMetadataFactory` class - instead. - - * Not defining the 6th argument `$format = null` of the - `AbstractNormalizer::instantiateObject()` method when overriding it is not - supported anymore. - - * Extending `ChainDecoder`, `ChainEncoder`, `ArrayDenormalizer` is not supported - anymore. - -Translation ------------ - - * Removed the backup feature from the file dumper classes. - - * The default value of the `$readerServiceId` argument of `TranslatorPass::__construct()` has been changed to `"translation.reader"`. - - * Removed `Symfony\Component\Translation\Writer\TranslationWriter::writeTranslations`, - use `Symfony\Component\Translation\Writer\TranslationWriter::write` instead. - - * Removed support for passing `Symfony\Component\Translation\MessageSelector` as a second argument to the - `Translator::__construct()`. You should pass an instance of `Symfony\Component\Translation\Formatter\MessageFormatterInterface` instead. - -TwigBundle ----------- - - * The `ContainerAwareRuntimeLoader` class has been removed. Use the - Twig `Twig_ContainerRuntimeLoader` class instead. - - * Removed `DebugCommand` in favor of `Symfony\Bridge\Twig\Command\DebugCommand`. - - * Removed `ContainerAwareInterface` implementation in `Symfony\Bundle\TwigBundle\Command\LintCommand`. - -TwigBridge ----------- - - * removed the `Symfony\Bridge\Twig\Form\TwigRenderer` class, use the `FormRenderer` - class from the Form component instead - - * Removed the possibility to inject the Form `TwigRenderer` into the `FormExtension`. - Upgrade Twig to `^1.30`, inject the `Twig_Environment` into the `TwigRendererEngine` and load - the `TwigRenderer` using the `Twig_FactoryRuntimeLoader` instead. - - Before: - - ```php - use Symfony\Bridge\Twig\Extension\FormExtension; - use Symfony\Bridge\Twig\Form\TwigRenderer; - use Symfony\Bridge\Twig\Form\TwigRendererEngine; - - // ... - $rendererEngine = new TwigRendererEngine(['form_div_layout.html.twig']); - $rendererEngine->setEnvironment($twig); - $twig->addExtension(new FormExtension(new TwigRenderer($rendererEngine, $csrfTokenManager))); - ``` - - After: - - ```php - $rendererEngine = new TwigRendererEngine(['form_div_layout.html.twig'], $twig); - $twig->addRuntimeLoader(new \Twig_FactoryRuntimeLoader([ - TwigRenderer::class => function () use ($rendererEngine, $csrfTokenManager) { - return new TwigRenderer($rendererEngine, $csrfTokenManager); - }, - ])); - $twig->addExtension(new FormExtension()); - ``` - - * Removed the `TwigRendererEngineInterface` interface. - - * The `TwigRendererEngine::setEnvironment()` method has been removed. - Pass the Twig Environment as second argument of the constructor instead. - - * Removed `DebugCommand::set/getTwigEnvironment`. Pass an instance of - `Twig\Environment` as first argument of the constructor instead. - - * Removed `LintCommand::set/getTwigEnvironment`. Pass an instance of - `Twig\Environment` as first argument of the constructor instead. - -Validator ---------- - - * The default value of the `strict` option of the `Choice` constraint was changed - to `true`. Using any other value will throw an exception. - - * The `DateTimeValidator::PATTERN` constant was removed. - - * `Tests\Constraints\AbstractConstraintValidatorTest` has been removed in - favor of `Test\ConstraintValidatorTestCase`. - - Before: - - ```php - // ... - use Symfony\Component\Validator\Tests\Constraints\AbstractConstraintValidatorTest; - - class MyCustomValidatorTest extends AbstractConstraintValidatorTest - { - // ... - } - ``` - - After: - - ```php - // ... - use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; - - class MyCustomValidatorTest extends ConstraintValidatorTestCase - { - // ... - } - ``` - - * Setting the `checkDNS` option of the `Url` constraint to `true` is dropped - in favor of `Url::CHECK_DNS_TYPE_*` constants values. - - Before: - - ```php - $constraint = new Url(['checkDNS' => true]); - ``` - - After: - - ```php - $constraint = new Url(['checkDNS' => Url::CHECK_DNS_TYPE_ANY]); - ``` - -VarDumper ---------- - - * The `VarDumperTestTrait::assertDumpEquals()` method expects a 3rd `$filter = 0` - argument and moves `$message = ''` argument at 4th position. - - Before: - - ```php - VarDumperTestTrait::assertDumpEquals($dump, $data, $message = ''); - ``` - - After: - - ```php - VarDumperTestTrait::assertDumpEquals($dump, $data, $filter = 0, $message = ''); - ``` - - * The `VarDumperTestTrait::assertDumpMatchesFormat()` method expects a 3rd `$filter = 0` - argument and moves `$message = ''` argument at 4th position. - - Before: - - ```php - VarDumperTestTrait::assertDumpMatchesFormat($dump, $data, $message = ''); - ``` - - After: - - ```php - VarDumperTestTrait::assertDumpMatchesFormat($dump, $data, $filter = 0, $message = ''); - ``` - -WebProfilerBundle ------------------ - - * Removed the `getTemplates()` method of the `TemplateManager` class in favor - of the `getNames()` method - -Workflow --------- - - * Removed class name support in `WorkflowRegistry::add()` as second parameter. - -Yaml ----- - - * Support for the `!str` tag was removed, use the `!!str` tag instead. - - * Starting an unquoted string with a question mark followed by a space - throws a `ParseException`. - - * Removed support for implicitly parsing non-string mapping keys as strings. - Mapping keys that are no strings will result in a `ParseException`. Use - quotes to opt-in for keys to be parsed as strings. - - Before: - - ```php - $yaml = << new A(), 'bar' => 1], 0, 0, true); - ``` - - After: - - ```php - Yaml::dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE); - ``` - - * Removed support for passing `true`/`false` as the fifth argument to the - `dump()` method to toggle object support. - - Before: - - ```php - Yaml::dump(['foo' => new A(), 'bar' => 1], 0, 0, false, true); - ``` - - After: - - ```php - Yaml::dump(['foo' => new A(), 'bar' => 1], 0, 0, false, Yaml::DUMP_OBJECT); - ``` - - * The `!!php/object` tag to indicate dumped PHP objects was removed in favor of - the `!php/object` tag. - - * Duplicate mapping keys lead to a `ParseException`. - - * The constructor arguments `$offset`, `$totalNumberOfLines` and - `$skippedLineNumbers` of the `Parser` class were removed. - - * The behavior of the non-specific tag `!` is changed and now forces - non-evaluating your values. - - * The `!php/object:` tag was removed in favor of the `!php/object` tag (without - the colon). - - * The `!php/const:` tag was removed in favor of the `!php/const` tag (without - the colon). - - Before: - - ```yml - !php/const:PHP_INT_MAX - ``` - - After: - - ```yml - !php/const PHP_INT_MAX - ``` diff --git a/UPGRADE-4.1.md b/UPGRADE-4.1.md deleted file mode 100644 index 8410c67f84e99..0000000000000 --- a/UPGRADE-4.1.md +++ /dev/null @@ -1,164 +0,0 @@ -UPGRADE FROM 4.0 to 4.1 -======================= - -Config ------- - - * Implementing `ParentNodeDefinitionInterface` without the `getChildNodeDefinitions()` method - is deprecated. - -Console -------- - - * Deprecated the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`. - * The `Processor` class has been made final - * Deprecated the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`. - * Deprecated the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`. - * Deprecated the `setVerticalBorderChar()` method in favor of the `setVerticalBorderChars()` method in `TableStyle`. - * Deprecated the `getVerticalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`. - * Added support for `iterable` messages in `write` and `writeln` methods of `Symfony\Component\Console\Output\OutputInterface`. - If you have a custom implementation of the interface, you should make sure it works with iterable as well. - -DependencyInjection -------------------- - - * Deprecated the `TypedReference::canBeAutoregistered()` and `TypedReference::getRequiringClass()` methods. - * Deprecated support for auto-discovered extension configuration class which does not implement `ConfigurationInterface`. - -EventDispatcher ---------------- - - * The `TraceableEventDispatcherInterface` has been deprecated. - -Form ----- - - * Deprecated the `ChoiceLoaderInterface` implementation in `CountryType`, - `LanguageType`, `LocaleType` and `CurrencyType`, use the `choice_loader` - option instead. - - Before: - ```php - class MyCountryType extends CountryType - { - public function loadChoiceList() - { - // override the method - } - } - ``` - - After: - ```php - class MyCountryType extends AbstractType - { - public function getParent() - { - return CountryType::class; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefault('choice_loader', ...); // override the option instead - } - } - ``` - - * Added `help` option to the form field. If you have custom Form extension for it, you should remove it. - Also remove it from the custom form theme. - -FrameworkBundle ---------------- - - * Deprecated `bundle:controller:action` and `service:action` syntaxes to reference controllers. Use `serviceOrFqcn::method` - instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller. - - Before: - - ```yml - bundle_controller: - path: / - defaults: - _controller: FrameworkBundle:Redirect:redirect - - service_controller: - path: / - defaults: - _controller: app.my_controller:myAction - ``` - - After: - - ```yml - bundle_controller: - path: / - defaults: - _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction - - service_controller: - path: / - defaults: - _controller: app.my_controller::myAction - ``` - - * Deprecated `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser` - * Warming up a router in `RouterCacheWarmer` that does not implement the `WarmableInterface` is deprecated and will not be - supported anymore in 5.0. - * The `RequestDataCollector` class has been deprecated. Use the `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector` class instead. - -HttpFoundation --------------- - - * Passing the file size to the constructor of the `UploadedFile` class is deprecated. - * The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead. - * Deprecated `Symfony\Component\HttpFoundation\Request::getSession()` when no session has been set. Use `Symfony\Component\HttpFoundation\Request::hasSession()` instead. - -Security --------- - - * The `ContextListener::setLogoutOnUserChange()` method is deprecated. - * Using the `AdvancedUserInterface` is now deprecated. To use the existing - functionality, create a custom user-checker based on the - `Symfony\Component\Security\Core\User\UserChecker`. - * `AuthenticationUtils::getLastUsername()` now always returns a string. - * The `ExpressionVoter::addExpressionLanguageProvider()` method is deprecated. Register the provider directly on the injected ExpressionLanguage instance instead. - -SecurityBundle --------------- - - * The `logout_on_user_change` firewall option is deprecated. - * The `switch_user.stateless` firewall option is deprecated, use the `stateless` option instead. - * The `SecurityUserValueResolver` class is deprecated, use - `Symfony\Component\Security\Http\Controller\UserValueResolver` instead. - -Serializer ----------- - - * Decoding XML with `XmlEncoder` now ignores comment node types by default. - -Translation ------------ - - * The `FileDumper::setBackup()` method is deprecated. - * The `TranslationWriter::disableBackup()` method is deprecated. - -TwigBundle ----------- - - * Deprecated relying on the default value (`false`) of the `twig.strict_variables` configuration option. You should use `%kernel.debug%` explicitly instead, which will be the new default in 5.0. - -Validator --------- - - * The `Email::__construct()` 'strict' property is deprecated. Use 'mode'=>"strict" instead. - * Calling `EmailValidator::__construct()` method with a boolean parameter is deprecated, use `EmailValidator("strict")` instead. - * Deprecated the `checkDNS` and `dnsMessage` options of the `Url` constraint. - -Workflow --------- - - * Deprecated the `DefinitionBuilder::reset()` method, use the `clear()` one instead. - * Deprecated the `add` method in favor of the `addWorkflow` method in `Workflow\Registry`. - * Deprecated `SupportStrategyInterface` in favor of `WorkflowSupportStrategyInterface`. - * Deprecated the class `ClassInstanceSupportStrategy` in favor of the class `InstanceOfSupportStrategy`. - * Deprecated passing the workflow name as 4th parameter of `Event` constructor in favor of the workflow itself. diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md deleted file mode 100644 index 8f7cc54411aa9..0000000000000 --- a/UPGRADE-4.2.md +++ /dev/null @@ -1,393 +0,0 @@ -UPGRADE FROM 4.1 to 4.2 -======================= - -BrowserKit ----------- - - * The `Client::submit()` method will have a new `$serverParameters` argument in version 5.0, not defining it is deprecated. - -Cache ------ - - * Deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead. - -Config ------- - - * Deprecated constructing a `TreeBuilder` without passing root node information: - - Before: - ```php - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('my_config'); - ``` - - After: - ```php - $treeBuilder = new TreeBuilder('my_config'); - $rootNode = $treeBuilder->getRootNode(); - ``` - - * Deprecated `FileLoaderLoadException`, use `LoaderLoadException` instead. - -Console -------- - - * Deprecated passing a command as a string to `ProcessHelper::run()`, - pass the command as an array of arguments instead. - - Before: - ```php - $processHelper->run($output, 'ls -l'); - ``` - - After: - ```php - $processHelper->run($output, array('ls', '-l')); - - // alternatively, when a shell wrapper is required - $processHelper->run($output, Process::fromShellCommandline('ls -l')); - ``` - -DoctrineBridge --------------- - - * The `lazy` attribute on `doctrine.event_listener` tags was removed. - Listeners are now lazy by default. So any `lazy` attributes can safely be removed from those tags. - -DomCrawler ----------- - - * The `Crawler::children()` method will have a new `$selector` argument in version 5.0, not defining it is deprecated. - -Finder ------- - - * The `Finder::sortByName()` method will have a new `$useNaturalSort` argument in version 5.0, not defining it is deprecated. - -Form ----- - - * The `symfony/translation` dependency has been removed - run `composer require symfony/translation` if you need the component - * The `getExtendedType()` method of the `FormTypeExtensionInterface` is deprecated and will be removed in 5.0. Type - extensions must implement the static `getExtendedTypes()` method instead and return an iterable of extended types. - - Before: - - ```php - class FooTypeExtension extends AbstractTypeExtension - { - public function getExtendedType() - { - return FormType::class; - } - - // ... - } - ``` - - After: - - ```php - class FooTypeExtension extends AbstractTypeExtension - { - public static function getExtendedTypes(): iterable - { - return array(FormType::class); - } - - // ... - } - ``` - * The `scale` option of the `IntegerType` is deprecated. - * The `$scale` argument of the `IntegerToLocalizedStringTransformer` is deprecated. - * Deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered. - Instead of expecting such calls to return empty strings, check if the field has already been rendered. - - Before: - ```twig - {% for field in fieldsWithPotentialDuplicates %} - {{ form_widget(field) }} - {% endfor %} - ``` - - After: - ```twig - {% for field in fieldsWithPotentialDuplicates if not field.rendered %} - {{ form_widget(field) }} - {% endfor %} - ``` - - * The `regions` option of the `TimezoneType` is deprecated. - -FrameworkBundle ---------------- - - * The following middleware service ids were renamed: - - `messenger.middleware.call_message_handler` becomes `messenger.middleware.handle_message` - - `messenger.middleware.route_messages` becomes `messenger.middleware.send_message` - - If you set `framework.messenger.buses.[bus_id].default_middleware` to `false`, - replace any of these names in the `framework.messenger.buses.[bus_id].middleware` list. - * The `allow_no_handler` middleware has been removed. Use `framework.messenger.buses.[bus_id].default_middleware` instead: - - Before: - ```yaml - framework: - messenger: - buses: - messenger.bus.events: - middleware: - - allow_no_handler - ``` - - After: - ```yaml - framework: - messenger: - buses: - messenger.bus.events: - default_middleware: allow_no_handlers - ``` - - * The `messenger:consume-messages` command expects a mandatory `--bus` option value if you have more than one bus configured. - * The `framework.router.utf8` configuration option has been added. If your app's charset - is UTF-8 (see kernel's `getCharset()` method), it is recommended to set it to `true`: - this will generate 404s for non-UTF-8 URLs, which are incompatible with you app anyway, - and will allow dumping optimized routers and using Unicode classes in requirements. - * Added support for the SameSite attribute for session cookies. It is highly recommended to set this setting (`framework.session.cookie_samesite`) to `lax` for increased security against CSRF attacks. - * The `Controller` class has been deprecated, use `AbstractController` instead. - * The Messenger encoder/decoder configuration has been changed for a unified Messenger serializer configuration. - - Before: - ```yaml - framework: - messenger: - encoder: your_encoder_service_id - decoder: your_decoder_service_id - ``` - - After: - ```yaml - framework: - messenger: - serializer: - id: your_messenger_service_id - ``` - * The `ContainerAwareCommand` class has been deprecated, use `Symfony\Component\Console\Command\Command` - with dependency injection instead. - * The `Templating\Helper\TranslatorHelper::transChoice()` method has been deprecated, use the `trans()` one instead with a `%count%` parameter. - * Deprecated support for legacy translations directories `src/Resources/translations/` and `src/Resources//translations/`, use `translations/` instead. - * Support for the legacy directory structure in `translation:update` and `debug:translation` commands has been deprecated. - -HttpFoundation --------------- - - * The default value of the `$secure` and `$samesite` arguments of Cookie's constructor - will respectively change from `false` to `null` and from `null` to `lax` in Symfony - 5.0, you should define their values explicitly or use `Cookie::create()` instead. - -HttpKernel ----------- - - * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been deprecated - * The `KernelInterface::getName()` and the `kernel.name` parameter have been deprecated - * Deprecated the first and second constructor argument of `ConfigDataCollector` - * Deprecated `ConfigDataCollector::getApplicationName()` - * Deprecated `ConfigDataCollector::getApplicationVersion()` - -Messenger ---------- - - * The `MiddlewareInterface::handle()` and `SenderInterface::send()` methods must now return an `Envelope` instance. - * The return value of handlers isn't forwarded anymore by middleware and buses. - If you used to return a value, e.g in query bus handlers, you can either: - - get the result from the `HandledStamp` in the envelope returned by the bus. - - use the `HandleTrait` to leverage a message bus, expecting a single, synchronous message handling and returning its result. - - make your `Query` mutable to allow setting & getting a result: - ```php - // When dispatching: - $bus->dispatch($query = new Query()); - $result = $query->getResult(); - - // In your handler: - $query->setResult($yourResult); - ``` - * The `EnvelopeAwareInterface` was removed and the `MiddlewareInterface::handle()` method now requires an `Envelope` object - as first argument. When using built-in middleware with the provided `MessageBus`, you will not have to do anything. - If you use your own `MessageBusInterface` implementation, you must wrap the message in an `Envelope` before passing it to middleware. - If you created your own middleware, you must change the signature to always expect an `Envelope`. - * The `MiddlewareInterface::handle()` second argument (`callable $next`) has changed in favor of a `StackInterface` instance. - When using built-in middleware with the provided `MessageBus`, you will not have to do anything. - If you use your own `MessageBusInterface` implementation, you can use the `StackMiddleware` implementation. - If you created your own middleware, you must change the signature to always expect an `StackInterface` instance - and call `$stack->next()->handle($envelope, $stack)` instead of `$next` to call the next middleware: - - Before: - ```php - public function handle($message, callable $next): Envelope - { - // do something before - $message = $next($message); - // do something after - - return $message; - } - ``` - - After: - ```php - public function handle(Envelope $envelope, StackInterface $stack): Envelope - { - // do something before - $envelope = $stack->next()->handle($envelope, $stack); - // do something after - - return $envelope; - } - ``` - * `StampInterface` replaces `EnvelopeItemInterface` and doesn't extend `Serializable` anymore. - Built-in `ReceivedMessage`, `ValidationConfiguration` and `SerializerConfiguration` were renamed - respectively `ReceivedStamp`, `ValidationStamp`, `SerializerStamp` and moved to the `Stamp` namespace. - * `AllowNoHandlerMiddleware` has been removed in favor of a new constructor argument on `HandleMessageMiddleware` - * The `ConsumeMessagesCommand` class now takes an instance of `Psr\Container\ContainerInterface` - as first constructor argument, i.e a message bus locator. The CLI command now expects a mandatory - `--bus` option value if there is more than one bus in the locator. - * `MessageSubscriberInterface::getHandledMessages()` return value has changed. The value of an array item - needs to be an associative array or the method name. - - Before: - ```php - return [ - [FirstMessage::class, 0], - [SecondMessage::class, -10], - ]; - ``` - - After: - ```php - yield FirstMessage::class => ['priority' => 0]; - yield SecondMessage::class => ['priority' => -10]; - ``` - - Before: - ```php - return [ - SecondMessage::class => ['secondMessageMethod', 20], - ]; - ``` - - After: - ```php - yield SecondMessage::class => [ - 'method' => 'secondMessageMethod', - 'priority' => 20, - ]; - ``` - * The `EncoderInterface` and `DecoderInterface` interfaces have been replaced by a unified `Symfony\Component\Messenger\Transport\Serialization\SerializerInterface`. - Each interface method have been merged untouched into the `Serializer` interface, so you can simply merge your two implementations together and implement the new interface. - * The `HandlerLocator` class was replaced with `Symfony\Component\Messenger\Handler\HandlersLocator`. - - Before: - ```php - new HandlerLocator([ - YourMessage::class => $handlerCallable, - ]); - ``` - - After: - ```php - new HandlersLocator([ - YourMessage::class => [ - $handlerCallable, - ] - ]); - ``` - -Monolog -------- - - * The methods `DebugProcessor::getLogs()`, `DebugProcessor::countErrors()`, `Logger::getLogs()` and `Logger::countErrors()` will have a new `$request` argument in version 5.0, not defining it is deprecated. - -Process -------- - - * Deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods. - * Deprecated passing commands as strings when creating a `Process` instance. - - Before: - ```php - $process = new Process('ls -l'); - ``` - - After: - ```php - $process = new Process(array('ls', '-l')); - - // alternatively, when a shell wrapper is required - $process = Process::fromShellCommandline('ls -l'); - ``` - -Security --------- - - * Using the `has_role()` function in security expressions is deprecated, use the `is_granted()` function instead. - * Not returning an array of 3 elements from `FirewallMapInterface::getListeners()` is deprecated, the 3rd element - must be an instance of `LogoutListener` or `null`. - * Passing custom class names to the - `Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver` to define - custom anonymous and remember me token classes is deprecated. To - use custom tokens, extend the existing `Symfony\Component\Security\Core\Authentication\Token\AnonymousToken` - or `Symfony\Component\Security\Core\Authentication\Token\RememberMeToken`. - * Accessing the user object that is not an instance of `UserInterface` from `Security::getUser()` is deprecated. - * `SimpleAuthenticatorInterface`, `SimpleFormAuthenticatorInterface`, `SimplePreAuthenticatorInterface`, - `SimpleAuthenticationProvider`, `SimpleAuthenticationHandler`, `SimpleFormAuthenticationListener` and - `SimplePreAuthenticationListener` have been deprecated. Use Guard instead. - * **BC break note**: Upgrade to this version will log out all logged in users. See bug #33473. - -SecurityBundle --------------- - - * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor is deprecated, - pass a `LogoutListener` instance instead. - * Using the `security.authentication.trust_resolver.anonymous_class` and - `security.authentication.trust_resolver.rememberme_class` parameters to define - the token classes is deprecated. To use - custom tokens extend the existing AnonymousToken and RememberMeToken. - * The `simple_form` and `simple_preauth` authentication listeners have been deprecated, - use Guard instead. - * The `SimpleFormFactory` and `SimplePreAuthenticationFactory` classes have been deprecated, - use Guard instead. - -Serializer ----------- - - * Relying on the default value (false) of the "as_collection" option is deprecated. - You should set it to false explicitly instead as true will be the default value in 5.0. - * The `AbstractNormalizer::handleCircularReference()` method will have two new `$format` and `$context` arguments in version 5.0, not defining them is deprecated. - -Translation ------------ - - * The `TranslatorInterface` has been deprecated in favor of `Symfony\Contracts\Translation\TranslatorInterface` - * The `Translator::transChoice()` method has been deprecated in favor of using `Translator::trans()` with "%count%" as the parameter driving plurals - * The `MessageSelector`, `Interval` and `PluralizationRules` classes have been deprecated, use `IdentityTranslator` instead - * The `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` method have been marked as internal - -TwigBundle ----------- - - * The `transchoice` tag and filter have been deprecated, use the `trans` ones instead with a `%count%` parameter. - * Deprecated support for legacy templates directories `src/Resources/views/` and `src/Resources//views/`, use `templates/` and `templates/bundles//` instead. - -Validator ---------- - - * The `symfony/translation` dependency has been removed - run `composer require symfony/translation` if you need the component - * The `checkMX` and `checkHost` options of the `Email` constraint are deprecated - * The component is now decoupled from `symfony/translation` and uses `Symfony\Contracts\Translation\TranslatorInterface` instead - * The `ValidatorBuilderInterface` has been deprecated and `ValidatorBuilder::setTranslator()` has been made final - * Deprecated validating instances of `\DateTimeInterface` in `DateTimeValidator`, `DateValidator` and `TimeValidator`. Use `Type` instead or remove the constraint if the underlying model is type hinted to `\DateTimeInterface` already. - * Using the `Bic`, `Country`, `Currency`, `Language` and `Locale` constraints without `symfony/intl` is deprecated - * Using the `Email` constraint in strict mode without `egulias/email-validator` is deprecated - * Using the `Expression` constraint without `symfony/expression-language` is deprecated diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md deleted file mode 100644 index 5093de27cb2dc..0000000000000 --- a/UPGRADE-4.3.md +++ /dev/null @@ -1,358 +0,0 @@ -UPGRADE FROM 4.2 to 4.3 -======================= - -BrowserKit ----------- - - * Renamed `Client` to `AbstractBrowser` - * Marked `Response` final. - * Deprecated `Response::buildHeader()` - * Deprecated `Response::getStatus()`, use `Response::getStatusCode()` instead - -Cache ------ - - * The `psr/simple-cache` dependency has been removed - run `composer require psr/simple-cache` if you need it. - * Deprecated all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead. - * Deprecated `SimpleCacheAdapter`, use `Psr16Adapter` instead. - -Config ------- - - * Deprecated using environment variables with `cannotBeEmpty()` if the value is validated with `validate()` - * Deprecated the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead - -DependencyInjection -------------------- - - * Deprecated support for non-string default env() parameters - - Before: - ```yaml - parameters: - env(NAME): 1.5 - ``` - - After: - ```yaml - parameters: - env(NAME): '1.5' - ``` - -Doctrine Bridge ---------------- - - * Passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field has been deprecated, pass `null` instead - * Not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field has been deprecated - -Dotenv ------- - - * First parameter of `Dotenv::__construct()` will be changed from `true` to `false` in Symfony 5.0. A deprecation warning - is triggered if no parameter is provided. Use `$usePutenv = true` to upgrade without breaking changes. - -EventDispatcher ---------------- - - * The signature of the `EventDispatcherInterface::dispatch()` method has been updated, consider using the new signature `dispatch($event, string $eventName = null)` instead of the old signature `dispatch($eventName, $event)` that is deprecated - - You have to swap arguments when calling `dispatch()`: - - Before: - ```php - $this->eventDispatcher->dispatch(Events::My_EVENT, $event); - ``` - - After: - ```php - $this->eventDispatcher->dispatch($event, Events::My_EVENT); - ``` - - If your bundle or package needs to provide compatibility with the previous way of using the dispatcher, you can use `Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy::decorate()` to ease upgrades: - - Before: - ```php - public function __construct(EventDispatcherInterface $eventDispatcher) { - $this->eventDispatcher = $eventDispatcher; - } - ``` - - After: - ```php - public function __construct(EventDispatcherInterface $eventDispatcher) { - $this->eventDispatcher = LegacyEventDispatcherProxy::decorate($eventDispatcher); - } - ``` - - * The `Event` class has been deprecated, use `Symfony\Contracts\EventDispatcher\Event` instead - -Filesystem ----------- - - * Support for passing arrays to `Filesystem::dumpFile()` is deprecated. - * Support for passing arrays to `Filesystem::appendToFile()` is deprecated. - -Form ----- - - * Using the `format` option of `DateType` and `DateTimeType` when the `html5` option is enabled is deprecated. - * Using names for buttons that do not start with a letter, a digit, or an underscore is deprecated and will lead to an - exception in 5.0. - * Using names for buttons that do not contain only letters, digits, underscores, hyphens, and colons is deprecated and - will lead to an exception in 5.0. - * Using the `date_format`, `date_widget`, and `time_widget` options of the `DateTimeType` when the `widget` option is - set to `single_text` is deprecated. - -FrameworkBundle ---------------- - - * Deprecated the `framework.templating` option, configure the Twig bundle instead. - * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will - be mandatory in 5.0. - * Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead. - * The `generate()` method of the `UrlGenerator` class can return an empty string instead of null. - -HttpFoundation --------------- - - * The `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` interfaces have been deprecated, - use `Symfony\Component\Mime\MimeTypesInterface` instead. - * The `MimeType` and `MimeTypeExtensionGuesser` classes have been deprecated, - use `Symfony\Component\Mime\MimeTypes` instead. - * The `FileBinaryMimeTypeGuesser` class has been deprecated, - use `Symfony\Component\Mime\FileBinaryMimeTypeGuesser` instead. - * The `FileinfoMimeTypeGuesser` class has been deprecated, - use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead. - -HttpKernel ----------- - - * Renamed `Client` to `HttpKernelBrowser` - * Renamed `FilterControllerArgumentsEvent` to `ControllerArgumentsEvent` - * Renamed `FilterControllerEvent` to `ControllerEvent` - * Renamed `FilterResponseEvent` to `ResponseEvent` - * Renamed `GetResponseEvent` to `RequestEvent` - * Renamed `GetResponseForControllerResultEvent` to `ViewEvent` - * Renamed `GetResponseForExceptionEvent` to `ExceptionEvent` - * Renamed `PostResponseEvent` to `TerminateEvent` - * Deprecated `TranslatorListener` in favor of `LocaleAwareListener` - -Intl ----- - - * Deprecated `ResourceBundle` namespace - * Deprecated `Intl::getCurrencyBundle()`, use `Currencies` instead - * Deprecated `Intl::getLanguageBundle()`, use `Languages` or `Scripts` instead - * Deprecated `Intl::getLocaleBundle()`, use `Locales` instead - * Deprecated `Intl::getRegionBundle()`, use `Countries` instead - -Messenger ---------- - - * `Amqp` transport does not throw `\AMQPException` anymore, catch `TransportException` instead. - * Deprecated the `LoggingMiddleware` class, pass a logger to `SendMessageMiddleware` instead. - -Routing -------- - - * The `generator_base_class`, `generator_cache_class`, `matcher_base_class`, and `matcher_cache_class` router - options have been deprecated. - * `Serializable` implementing methods for `Route` and `CompiledRoute` are marked as `@internal` and `@final`. - Instead of overwriting them, use `__serialize` and `__unserialize` as extension points which are forward compatible - with the new serialization methods in PHP 7.4. - -Security --------- - - * The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles - instead. - * The `getReachableRoles()` method of the `RoleHierarchyInterface` is deprecated and will be removed in 5.0. - Role hierarchies must implement the `getReachableRoleNames()` method instead and return roles as strings. - * The `getRoles()` method of the `TokenInterface` is deprecated. Tokens must implement the `getRoleNames()` - method instead and return roles as strings. - * The `ListenerInterface` is deprecated, turn your listeners into callables instead. - * The `Firewall::handleRequest()` method is deprecated, use `Firewall::callListeners()` instead. - * The `AbstractToken::serialize()`, `AbstractToken::unserialize()`, - `AuthenticationException::serialize()` and `AuthenticationException::unserialize()` - methods are now final, use `__serialize()` and `__unserialize()` instead. - - Before: - ```php - public function serialize() - { - return [$this->myLocalVar, parent::serialize()]; - } - - public function unserialize($serialized) - { - [$this->myLocalVar, $parentSerialized] = unserialize($serialized); - parent::unserialize($parentSerialized); - } - ``` - - After: - ```php - public function __serialize(): array - { - return [$this->myLocalVar, parent::__serialize()]; - } - - public function __unserialize(array $data): void - { - [$this->myLocalVar, $parentData] = $data; - parent::__unserialize($parentData); - } - ``` - - * The `Argon2iPasswordEncoder` class has been deprecated, use `SodiumPasswordEncoder` instead. - * The `BCryptPasswordEncoder` class has been deprecated, use `NativePasswordEncoder` instead. - * Not implementing the methods `__serialize` and `__unserialize` in classes implementing - the `TokenInterface` is deprecated - -TwigBridge ----------- - - * deprecated the `$requestStack` and `$requestContext` arguments of the - `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` - instance as the only argument instead - -Workflow --------- - - * `initial_place` is deprecated in favour of `initial_marking`. - - Before: - ```yaml - framework: - workflows: - article: - initial_place: draft - ``` - - After: - ```yaml - framework: - workflows: - article: - initial_marking: [draft] - ``` - - * `WorkflowInterface::apply()` will have a third argument in Symfony 5.0. - - Before: - ```php - class MyWorkflow implements WorkflowInterface - { - public function apply($subject, $transitionName) - { - } - } - ``` - - After: - ```php - class MyWorkflow implements WorkflowInterface - { - public function apply($subject, $transitionName, array $context = []) - { - } - } - ``` - - * `MarkingStoreInterface::setMarking()` will have a third argument in Symfony 5.0. - - Before: - ```php - class MyMarkingStore implements MarkingStoreInterface - { - public function setMarking($subject, Marking $marking) - { - } - } - ``` - - After: - ```php - class MyMarkingStore implements MarkingStoreInterface - { - public function setMarking($subject, Marking $marking , array $context = []) - { - } - } - ``` - - * `MultipleStateMarkingStore` is deprecated. Use `MethodMarkingStore` instead. - - Before: - ```yaml - framework: - workflows: - article: - type: workflow - marking_store: - type: multiple_state - arguments: states - ``` - - After: - ```yaml - framework: - workflows: - article: - type: workflow - marking_store: - type: method - property: states - ``` - - * `SingleStateMarkingStore` is deprecated. Use `MethodMarkingStore` instead. - - Before: - ```yaml - framework: - workflows: - article: - marking_store: - arguments: state - ``` - - After: - ```yaml - framework: - workflows: - article: - type: state_machine - marking_store: - type: method - property: state - ``` - - * Using a workflow with a single state marking is deprecated. Use a state machine instead. - - Before: - ```yaml - framework: - workflows: - article: - type: workflow - marking_store: - type: single_state - ``` - - After: - ```yaml - framework: - workflows: - article: - type: state_machine - marking_store: - # type: single_state # Since the single_state marking store is deprecated, use method instead - type: method - ``` - - * Using `DefinitionBuilder::setInitialPlace()` is deprecated, use `DefinitionBuilder::setInitialPlaces()` instead. - -Yaml ----- - - * Using a mapping inside a multi-line string is deprecated and will throw a `ParseException` in 5.0. diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md deleted file mode 100644 index c0160192746c3..0000000000000 --- a/UPGRADE-4.4.md +++ /dev/null @@ -1,396 +0,0 @@ -UPGRADE FROM 4.3 to 4.4 -======================= - -Cache ------ - - * Added argument `$prefix` to `AdapterInterface::clear()` - * Marked the `CacheDataCollector` class as `@final`. - -Console -------- - - * Deprecated finding hidden commands using an abbreviation, use the full name instead - * Deprecated returning `null` from `Command::execute()`, return `0` instead - * Deprecated the `Application::renderException()` and `Application::doRenderException()` methods, - use `renderThrowable()` and `doRenderThrowable()` instead. - -Debug ------ - - * Deprecated the component in favor of the `ErrorHandler` component - * Replace uses of `Symfony\Component\Debug\Debug` by `Symfony\Component\ErrorHandler\Debug` - -Config ------- - - * Deprecated overriding the `FilerLoader::import()` method without declaring the optional `$exclude` argument - -DependencyInjection -------------------- - - * Made singly-implemented interfaces detection be scoped by file - * Deprecated support for short factories and short configurators in Yaml - - Before: - ```yaml - services: - my_service: - factory: factory_service:method - ``` - - After: - ```yaml - services: - my_service: - factory: ['@factory_service', method] - ``` - - * Passing an instance of `Symfony\Component\DependencyInjection\Parameter` as class name to `Symfony\Component\DependencyInjection\Definition` is deprecated. - - Before: - ```php - new Definition(new Parameter('my_class')); - ``` - - After: - ```php - new Definition('%my_class%'); - ``` - -DoctrineBridge --------------- - * Deprecated injecting `ClassMetadataFactory` in `DoctrineExtractor`, an instance of `EntityManagerInterface` should be - injected instead. - * Deprecated passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field. - * Deprecated not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field. - * Deprecated `RegistryInterface`, use `Doctrine\Persistence\ManagerRegistry`. - * Added a new `getMetadataDriverClass` method to replace class parameters in `AbstractDoctrineExtension`. This method - will be abstract in Symfony 5 and must be declared in extending classes. - -Filesystem ----------- - - * Support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated. - -Form ----- - - * Using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a - reference date is deprecated. - * Using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` is deprecated. - * Overriding the methods `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` without the `void` return-type is deprecated. - -FrameworkBundle ---------------- - - * Deprecated calling `WebTestCase::createClient()` while a kernel has been booted, ensure the kernel is shut down before calling the method - * Deprecated support for `templating` engine in `TemplateController`, use Twig instead - * The `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` - has been deprecated. - * The `ControllerResolver` and `DelegatingLoader` classes have been marked as `final`. - * The `controller_name_converter` and `resolve_controller_name_subscriber` services have been deprecated. - * Deprecated `routing.loader.service`, use `routing.loader.container` instead. - * Not tagging service route loaders with `routing.route_loader` has been deprecated. - * Overriding the methods `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` without the `void` return-type is deprecated. - * Marked the `RouterDataCollector` class as `@final`. - -HttpClient ----------- - - * Added method `cancel()` to `ResponseInterface` - -HttpFoundation --------------- - - * `ApacheRequest` is deprecated, use `Request` class instead. - * Passing a third argument to `HeaderBag::get()` is deprecated since Symfony 4.4, use method `all()` instead - * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, - make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to - update your database. - * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, - make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database - to speed up garbage collection of expired sessions. - -HttpKernel ----------- - - * The `DebugHandlersListener` class has been marked as `final` - * Added new Bundle directory convention consistent with standard skeletons: - - ``` - └── MyBundle/ - ├── config/ - ├── public/ - ├── src/ - │ └── MyBundle.php - ├── templates/ - └── translations/ - ``` - - To make this work properly, it is necessary to change the root path of the bundle: - - ```php - class MyBundle extends Bundle - { - public function getPath(): string - { - return \dirname(__DIR__); - } - } - ``` - - As many bundles must be compatible with a range of Symfony versions, the current - directory convention is not deprecated yet, but it will be in the future. - - * Deprecated the second and third argument of `KernelInterface::locateResource` - * Deprecated the second and third argument of `FileLocator::__construct` - * Deprecated loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as - fallback directories. Resources like service definitions are usually loaded relative to the - current directory or with a glob pattern. The fallback directories have never been advocated - so you likely do not use those in any app based on the SF Standard or Flex edition. - * Getting the container from a non-booted kernel is deprecated - * Marked the `AjaxDataCollector`, `ConfigDataCollector`, `EventDataCollector`, - `ExceptionDataCollector`, `LoggerDataCollector`, `MemoryDataCollector`, - `RequestDataCollector` and `TimeDataCollector` classes as `@final`. - * Marked the `RouterDataCollector::collect()` method as `@final`. - * The `DataCollectorInterface::collect()` and `Profiler::collect()` methods third parameter signature - will be `\Throwable $exception = null` instead of `\Exception $exception = null` in Symfony 5.0. - * Deprecated methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead - * Deprecated class `ExceptionListener`, use `ErrorListener` instead - -Lock ----- - - * Deprecated `Symfony\Component\Lock\StoreInterface` in favor of `Symfony\Component\Lock\BlockingStoreInterface` and - `Symfony\Component\Lock\PersistingStoreInterface`. - * `Factory` is deprecated, use `LockFactory` instead - * Deprecated services `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract`, - use `StoreFactory::createStore` instead. - -Mailer ------- - - * [BC BREAK] Changed the DSN to use for disabling delivery (using the `NullTransport`) from `smtp://null` to `null://null` (host doesn't matter). - * [BC BREAK] Renamed class `SmtpEnvelope` to `Envelope` and `DelayedSmtpEnvelope` to `DelayedEnvelope`. - * [BC BREAK] Added a required `string $transport` argument to `MessageEvent::__construct`. - -Messenger ---------- - - * [BC BREAK] Removed `SendersLocatorInterface::getSenderByAlias` added in 4.3. - * [BC BREAK] Removed `$retryStrategies` argument from `Worker::__construct`. - * [BC BREAK] Changed arguments of `ConsumeMessagesCommand::__construct`. - * [BC BREAK] Removed `$senderClassOrAlias` argument from `RedeliveryStamp::__construct`. - * [BC BREAK] Removed `UnknownSenderException`. - * [BC BREAK] Removed `WorkerInterface`. - * [BC BREAK] Removed `$onHandledCallback` of `Worker::run(array $options = [], callable $onHandledCallback = null)`. - * [BC BREAK] Removed `StopWhenMemoryUsageIsExceededWorker` in favor of `StopWorkerOnMemoryLimitListener`. - * [BC BREAK] Removed `StopWhenMessageCountIsExceededWorker` in favor of `StopWorkerOnMessageLimitListener`. - * [BC BREAK] Removed `StopWhenTimeLimitIsReachedWorker` in favor of `StopWorkerOnTimeLimitListener`. - * [BC BREAK] Removed `StopWhenRestartSignalIsReceived` in favor of `StopWorkerOnRestartSignalListener`. - * Marked the `MessengerDataCollector` class as `@final`. - -Mime ----- - - * Removed `NamedAddress`, use `Address` instead (which supports a name now) - -MonologBridge --------------- - - * The `RouteProcessor` has been marked final. - -Process -------- - - * Deprecated the `Process::inheritEnvironmentVariables()` method: env variables are always inherited. - -PropertyAccess --------------- - - * Deprecated passing `null` as 2nd argument of `PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0` instead. - -Routing -------- - - * Deprecated `ServiceRouterLoader` in favor of `ContainerLoader`. - * Deprecated `ObjectRouteLoader` in favor of `ObjectLoader`. - -Security --------- - - * The `LdapUserProvider` class has been deprecated, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead. - * Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` should add a new `needsRehash()` method - * Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`. Please explicitly return `false` to indicate invalid credentials. - * The `ListenerInterface` is deprecated, extend `AbstractListener` instead. - * Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` (and indirectly the `is_granted()` Twig and ExpressionLanguage function) - - **Before** - ```php - if ($this->authorizationChecker->isGranted(['ROLE_USER', 'ROLE_ADMIN'])) { - // ... - } - ``` - - **After** - ```php - if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} - - // or: - if ($this->authorizationChecker->isGranted('ROLE_USER') - || $this->authorizationChecker->isGranted('ROLE_ADMIN') - ) {} - ``` - -SecurityBundle --------------- - - * Marked the `SecurityDataCollector` class as `@final`. - -Serializer ----------- - - * Deprecated the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant. Use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead. - -Stopwatch ---------- - - * Deprecated passing `null` as 1st (`$id`) argument of `Section::get()` method, pass a valid child section identifier instead. - -Translation ------------ - - * Deprecated support for using `null` as the locale in `Translator`. - * Deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. - * Marked the `TranslationDataCollector` class as `@final`. - -TwigBridge ----------- - - * Deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the - `DebugCommand::__construct()` method, swap the variables position. - * Deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. - * Marked the `TwigDataCollector` class as `@final`. - -TwigBundle ----------- - - * Deprecated `twig.exception_controller` configuration option. - - If you were not using this option previously, set it to `null`: - - After: - ```yaml - twig: - exception_controller: null - ``` - - If you were using this option previously, set it to `null` and use `framework.error_controller` instead: - - Before: - ```yaml - twig: - exception_controller: 'App\Controller\MyExceptionController' - ``` - - After: - ```yaml - twig: - exception_controller: null - - framework: - error_controller: 'App\Controller\MyExceptionController' - ``` - - The new default exception controller will also change the error response content according to - https://tools.ietf.org/html/rfc7807 for `json`, `xml`, `atom` and `txt` formats: - - Before (HTTP status code `200`): - ```json - { - "error": { - "code": 404, - "message": "Sorry, the page you are looking for could not be found" - } - } - ``` - - After (HTTP status code `404`): - ```json - { - "title": "Not Found", - "status": 404, - "detail": "Sorry, the page you are looking for could not be found" - } - ``` - - * Deprecated the `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the HttpKernel component instead - * Deprecated all built-in error templates, use the error renderer mechanism of the `ErrorHandler` component - * Deprecated loading custom error templates in non-html formats. Custom HTML error pages based on Twig keep working as before: - - Before (`templates/bundles/TwigBundle/Exception/error.json.twig`): - ```twig - { - "type": "https://example.com/error", - "title": "{{ status_text }}", - "status": {{ status_code }} - } - ``` - - After (`App\Serializer\ProblemJsonNormalizer`): - ```php - class ProblemJsonNormalizer implements NormalizerInterface - { - public function normalize($exception, $format = null, array $context = []) - { - return [ - 'type' => 'https://example.com/error', - 'title' => $exception->getStatusText(), - 'status' => $exception->getStatusCode(), - ]; - } - - public function supportsNormalization($data, $format = null) - { - return 'json' === $format && $data instanceof FlattenException; - } - } - ``` - -Validator ---------- - - * [BC BREAK] Using null as `$classValidatorRegexp` value in `DoctrineLoader::__construct` or `PropertyInfoLoader::__construct` will not enable auto-mapping for all classes anymore, use `'{.*}'` instead. - * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. - * Deprecated using anything else than a `string` as the code of a `ConstraintViolation`, a `string` type-hint will - be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()` - method in 5.0. - * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. - Pass it as the first argument instead. - * The `Length` constraint expects the `allowEmptyString` option to be defined - when the `min` option is used. - Set it to `true` to keep the current behavior and `false` to reject empty strings. - In 5.0, it'll become optional and will default to `false`. - * Overriding the methods `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` without the `void` return-type is deprecated. - * deprecated `Symfony\Component\Validator\Mapping\Cache\CacheInterface` and all implementations in favor of PSR-6. - * deprecated `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead. - * The `Range` constraint has a new message option `notInRangeMessage` that is used when both `min` and `max` values are set. - In case you are using custom translations make sure to add one for this new message. - * Marked the `ValidatorDataCollector` class as `@final`. - -WebProfilerBundle ------------------ - - * Deprecated the `ExceptionController` class in favor of `ExceptionErrorController` - * Deprecated the `TemplateManager::templateExists()` method - -WebServerBundle ---------------- - - * The bundle is deprecated and will be removed in 5.0. - -Yaml ----- - - * Deprecated accepting STDIN implicitly when using the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md deleted file mode 100644 index 67b5ef02c3787..0000000000000 --- a/UPGRADE-5.0.md +++ /dev/null @@ -1,684 +0,0 @@ -UPGRADE FROM 4.x to 5.0 -======================= - -BrowserKit ----------- - - * Removed `Client`, use `AbstractBrowser` instead - * Removed the possibility to extend `Response` by making it final. - * Removed `Response::buildHeader()` - * Removed `Response::getStatus()`, use `Response::getStatusCode()` instead - * The `Client::submit()` method has a new `$serverParameters` argument. - -Cache ------ - - * Removed `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead. - * Removed all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead. - * Removed `SimpleCacheAdapter`, use `Psr16Adapter` instead. - * Added argument `$prefix` to `AdapterInterface::clear()` - -Config ------- - - * Dropped support for constructing a `TreeBuilder` without passing root node information. - * Added the `getChildNodeDefinitions()` method to `ParentNodeDefinitionInterface`. - * The `Processor` class has been made final - * Removed `FileLoaderLoadException`, use `LoaderLoadException` instead. - * Using environment variables with `cannotBeEmpty()` if the value is validated with `validate()` will throw an exception. - * Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead - * The `FilerLoader::import()` method has a new `$exclude` argument. - -Console -------- - - * Removed support for finding hidden commands using an abbreviation, use the full name instead - * Removed the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`. - * Removed the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`. - * Removed the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`. - * Removed the `setVerticalBorderChar()` method in favor of the `setVerticalBorderChars()` method in `TableStyle`. - * Removed the `getVerticalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`. - * Removed support for returning `null` from `Command::execute()`, return `0` instead - * Renamed `Application::renderException()` and `Application::doRenderException()` - to `renderThrowable()` and `doRenderThrowable()` respectively. - * The `ProcessHelper::run()` method takes the command as an array of arguments. - - Before: - ```php - $processHelper->run($output, 'ls -l'); - ``` - - After: - ```php - $processHelper->run($output, array('ls', '-l')); - - // alternatively, when a shell wrapper is required - $processHelper->run($output, Process::fromShellCommandline('ls -l')); - ``` - -Debug ------ - - * Removed the component in favor of the `ErrorHandler` component - * Replace uses of `Symfony\Component\Debug\Debug` by `Symfony\Component\ErrorHandler\Debug` - -DependencyInjection -------------------- - - * Removed the `TypedReference::canBeAutoregistered()` and `TypedReference::getRequiringClass()` methods. - * Removed support for auto-discovered extension configuration class which does not implement `ConfigurationInterface`. - * Removed support for non-string default env() parameters - - Before: - ```yaml - parameters: - env(NAME): 1.5 - ``` - - After: - ```yaml - parameters: - env(NAME): '1.5' - ``` - - * Removed support for short factories and short configurators in Yaml - - Before: - ```yaml - services: - my_service: - factory: factory_service:method - ``` - - After: - ```yaml - services: - my_service: - factory: ['@factory_service', method] - ``` - -DoctrineBridge --------------- - - * Removed the possibility to inject `ClassMetadataFactory` in `DoctrineExtractor`, an instance of `EntityManagerInterface` should be - injected instead - * Passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field will throw an exception, pass `null` instead - * Not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field will not apply any optimization - * The `RegistryInterface` has been removed. - * Added a new `getMetadataDriverClass` method in `AbstractDoctrineExtension` to replace class parameters. - -DomCrawler ----------- - - * The `Crawler::children()` method has a new `$selector` argument. - -Dotenv ------- - - * First parameter `$usePutenv` of `Dotenv::__construct()` now default to `false`. - -EventDispatcher ---------------- - - * The `TraceableEventDispatcherInterface` has been removed. - * The signature of the `EventDispatcherInterface::dispatch()` method has been updated to `dispatch($event, string $eventName = null)` - * The `Event` class has been removed, use `Symfony\Contracts\EventDispatcher\Event` instead - -Filesystem ----------- - - * The `Filesystem::isAbsolutePath()` method no longer supports `null` in the `$file` argument. - * The `Filesystem::dumpFile()` method no longer supports arrays in the `$content` argument. - * The `Filesystem::appendToFile()` method no longer supports arrays in the `$content` argument. - -Finder ------- - - * The `Finder::sortByName()` method has a new `$useNaturalSort` argument. - -Form ----- - - * Removed support for using different values for the "model_timezone" and "view_timezone" options of the `TimeType` - without configuring a reference date. - * Removed support for using `int` or `float` as data for the `NumberType` when the `input` option is set to `string`. - * Removed support for using the `format` option of `DateType` and `DateTimeType` when the `html5` option is enabled. - * Using names for buttons that do not start with a letter, a digit, or an underscore leads to an exception. - * Using names for buttons that do not contain only letters, digits, underscores, hyphens, and colons leads to an - exception. - * Using the `date_format`, `date_widget`, and `time_widget` options of the `DateTimeType` when the `widget` option is - set to `single_text` is not supported anymore. - * The `getExtendedType()` method was removed from the `FormTypeExtensionInterface`. It is replaced by the the static - `getExtendedTypes()` method which must return an iterable of extended types. - - Before: - - ```php - class FooTypeExtension extends AbstractTypeExtension - { - public function getExtendedType() - { - return FormType::class; - } - - // ... - } - ``` - - After: - - ```php - class FooTypeExtension extends AbstractTypeExtension - { - public static function getExtendedTypes(): iterable - { - return array(FormType::class); - } - - // ... - } - ``` - * The `scale` option was removed from the `IntegerType`. - * The `$scale` argument of the `IntegerToLocalizedStringTransformer` was removed. - * Calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered - throws an exception instead of returning empty strings: - - Before: - ```twig - {% for field in fieldsWithPotentialDuplicates %} - {{ form_widget(field) }} - {% endfor %} - ``` - - After: - ```twig - {% for field in fieldsWithPotentialDuplicates if not field.rendered %} - {{ form_widget(field) }} - {% endfor %} - ``` - - * The `regions` option was removed from the `TimezoneType`. - * Added support for PHPUnit 8. A `void` return-type was added to the `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` methods. - -FrameworkBundle ---------------- - - * Calling `WebTestCase::createClient()` while a kernel has been booted now throws an exception, ensure the kernel is shut down before calling the method - * Removed the `framework.templating` option, configure the Twig bundle instead. - * The project dir argument of the constructor of `AssetsInstallCommand` is required. - * Removed support for `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method` - instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller. - - Before: - - ```yml - bundle_controller: - path: / - defaults: - _controller: FrameworkBundle:Redirect:redirect - ``` - - After: - - ```yml - bundle_controller: - path: / - defaults: - _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction - ``` - - * Removed `Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser`. - * Warming up a router in `RouterCacheWarmer` that does not implement the `WarmableInterface` is not supported anymore. - * The `RequestDataCollector` class has been removed. Use the `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector` class instead. - * Removed `Symfony\Bundle\FrameworkBundle\Controller\Controller`. Use `Symfony\Bundle\FrameworkBundle\Controller\AbstractController` instead. - * Added support for the SameSite attribute for session cookies. It is highly recommended to set this setting (`framework.session.cookie_samesite`) to `lax` for increased security against CSRF attacks. - * The `ContainerAwareCommand` class has been removed, use `Symfony\Component\Console\Command\Command` - with dependency injection instead. - * The `Templating\Helper\TranslatorHelper::transChoice()` method has been removed, use the `trans()` one instead with a `%count%` parameter. - * Removed support for legacy translations directories `src/Resources/translations/` and `src/Resources//translations/`, use `translations/` instead. - * Support for the legacy directory structure in `translation:update` and `debug:translation` commands has been removed. - * Removed the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead. - * Removed support for `templating` engine in `TemplateController`, use Twig instead - * Removed `ResolveControllerNameSubscriber`. - * Removed `routing.loader.service`. - * Added support for PHPUnit 8. A `void` return-type was added to the `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` method. - * Removed the `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract` services. - * Removed the `router.cache_class_prefix` parameter. - -HttpClient ----------- - - * Added method `cancel()` to `ResponseInterface` - * The `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` - has been removed. - * The `ControllerResolver` and `DelegatingLoader` classes have been made `final`. - * The `controller_name_converter` and `resolve_controller_name_subscriber` services have been removed. - -HttpFoundation --------------- - - * The `$size` argument of the `UploadedFile` constructor has been removed. - * The `getClientSize()` method of the `UploadedFile` class has been removed. - * The `getSession()` method of the `Request` class throws an exception when session is null. - * The default value of the "$secure" and "$samesite" arguments of Cookie's constructor - changed respectively from "false" to "null" and from "null" to "lax". - * The `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` interfaces have been removed, - use `Symfony\Component\Mime\MimeTypesInterface` instead. - * The `MimeType` and `MimeTypeExtensionGuesser` classes have been removed, - use `Symfony\Component\Mime\MimeTypes` instead. - * The `FileBinaryMimeTypeGuesser` class has been removed, - use `Symfony\Component\Mime\FileBinaryMimeTypeGuesser` instead. - * The `FileinfoMimeTypeGuesser` class has been removed, - use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead. - * `ApacheRequest` has been removed, use the `Request` class instead. - * The third argument of the `HeaderBag::get()` method has been removed, use method `all()` instead. - * Getting the container from a non-booted kernel is not possible anymore. - * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, - make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to - update your database. - -HttpKernel ----------- - - * Removed `Client`, use `HttpKernelBrowser` instead - * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been removed - * The `KernelInterface::getName()` and the `kernel.name` parameter have been removed - * Removed the first and second constructor argument of `ConfigDataCollector` - * Removed `ConfigDataCollector::getApplicationName()` - * Removed `ConfigDataCollector::getApplicationVersion()` - * Removed `FilterControllerArgumentsEvent`, use `ControllerArgumentsEvent` instead - * Removed `FilterControllerEvent`, use `ControllerEvent` instead - * Removed `FilterResponseEvent`, use `ResponseEvent` instead - * Removed `GetResponseEvent`, use `RequestEvent` instead - * Removed `GetResponseForControllerResultEvent`, use `ViewEvent` instead - * Removed `GetResponseForExceptionEvent`, use `ExceptionEvent` instead - * Removed `PostResponseEvent`, use `TerminateEvent` instead - * Removed `TranslatorListener` in favor of `LocaleAwareListener` - * The `DebugHandlersListener` class has been made `final` - * Removed `SaveSessionListener` in favor of `AbstractSessionListener` - * Removed methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead - * Removed class `ExceptionListener`, use `ErrorListener` instead - * Added new Bundle directory convention consistent with standard skeletons: - - ``` - └── MyBundle/ - ├── config/ - ├── public/ - ├── src/ - │ └── MyBundle.php - ├── templates/ - └── translations/ - ``` - - To make this work properly, it is necessary to change the root path of the bundle: - - ```php - class MyBundle extends Bundle - { - public function getPath(): string - { - return \dirname(__DIR__); - } - } - ``` - - As many bundles must be compatible with a range of Symfony versions, the current - directory convention is not deprecated yet, but it will be in the future. - * Removed the second and third argument of `KernelInterface::locateResource` - * Removed the second and third argument of `FileLocator::__construct` - * Removed loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as - fallback directories. - -Intl ----- - - * Removed `ResourceBundle` namespace - * Removed `Intl::getLanguageBundle()`, use `Languages` or `Scripts` instead - * Removed `Intl::getCurrencyBundle()`, use `Currencies` instead - * Removed `Intl::getLocaleBundle()`, use `Locales` instead - * Removed `Intl::getRegionBundle()`, use `Countries` instead - -Lock ----- - - * Removed `Symfony\Component\Lock\StoreInterface` in favor of `Symfony\Component\Lock\BlockingStoreInterface` and - `Symfony\Component\Lock\PersistingStoreInterface`. - * Removed `Factory`, use `LockFactory` instead - -Messenger ---------- - - * The `LoggingMiddleware` class has been removed, pass a logger to `SendMessageMiddleware` instead. - * Passing a `ContainerInterface` instance as first argument of the `ConsumeMessagesCommand` constructor now - throws as `\TypeError`, pass a `RoutableMessageBus` instance instead. - -Monolog -------- - - * The methods `DebugProcessor::getLogs()`, `DebugProcessor::countErrors()`, `Logger::getLogs()` and `Logger::countErrors()` have a new `$request` argument. - -MonologBridge --------------- - - * The `RouteProcessor` class is final. - -Process -------- - - * Removed the `Process::inheritEnvironmentVariables()` method: env variables are always inherited. - * Removed the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods. - * Commands must be defined as arrays when creating a `Process` instance. - - Before: - ```php - $process = new Process('ls -l'); - ``` - - After: - ```php - $process = new Process(array('ls', '-l')); - - // alternatively, when a shell wrapper is required - $process = Process::fromShellCommandline('ls -l'); - ``` - -PropertyAccess --------------- - - * Removed support of passing `null` as 2nd argument of - `PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0` - instead. - -Routing -------- - - * The `generator_base_class`, `generator_cache_class`, `matcher_base_class`, and `matcher_cache_class` router - options have been removed. If you are using multiple Router instances and need separate caches for them, set a unique `cache_dir` per Router instance instead. - * `Serializable` implementing methods for `Route` and `CompiledRoute` are final. - Instead of overwriting them, use `__serialize` and `__unserialize` as extension points which are forward compatible - with the new serialization methods in PHP 7.4. - * Removed `ServiceRouterLoader` and `ObjectRouteLoader`. - * Service route loaders must be tagged with `routing.route_loader`. - * The `RoutingConfigurator::import()` method has a new optional `$exclude` argument. - -Security --------- - - * Dropped support for passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` (and indirectly the `is_granted()` Twig and ExpressionLanguage function): - - **Before** - ```php - if ($this->authorizationChecker->isGranted(['ROLE_USER', 'ROLE_ADMIN'])) { - // ... - } - ``` - - **After** - ```php - if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} - - // or: - if ($this->authorizationChecker->isGranted('ROLE_USER') - || $this->authorizationChecker->isGranted('ROLE_ADMIN') - ) {} - ``` - * The `LdapUserProvider` class has been removed, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead. - * Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` must have a new `needsRehash()` method - * The `Role` and `SwitchUserRole` classes have been removed. - * The `getReachableRoles()` method of the `RoleHierarchy` class has been removed. It has been replaced by the new - `getReachableRoleNames()` method. - * The `getRoles()` method has been removed from the `TokenInterface`. It has been replaced by the new - `getRoleNames()` method. - * The `ContextListener::setLogoutOnUserChange()` method has been removed. - * The `Symfony\Component\Security\Core\User\AdvancedUserInterface` has been removed. - * The `ExpressionVoter::addExpressionLanguageProvider()` method has been removed. - * The `FirewallMapInterface::getListeners()` method must return an array of 3 elements, - the 3rd one must be either a `LogoutListener` instance or `null`. - * The `AuthenticationTrustResolver` constructor arguments have been removed. - * A user object that is not an instance of `UserInterface` cannot be accessed from `Security::getUser()` anymore and returns `null` instead. - * `SimpleAuthenticatorInterface`, `SimpleFormAuthenticatorInterface`, `SimplePreAuthenticatorInterface`, - `SimpleAuthenticationProvider`, `SimpleAuthenticationHandler`, `SimpleFormAuthenticationListener` and - `SimplePreAuthenticationListener` have been removed. Use Guard instead. - * The `ListenerInterface` has been removed, extend `AbstractListener` instead. - * The `Firewall::handleRequest()` method has been removed, use `Firewall::callListeners()` instead. - * `\Serializable` interface has been removed from `AbstractToken` and `AuthenticationException`, - thus `serialize()` and `unserialize()` aren't available. - Use `__serialize()` and `__unserialize()` instead. - - Before: - ```php - public function serialize() - { - return [$this->myLocalVar, parent::serialize()]; - } - - public function unserialize($serialized) - { - [$this->myLocalVar, $parentSerialized] = unserialize($serialized); - parent::unserialize($parentSerialized); - } - ``` - - After: - ```php - public function __serialize(): array - { - return [$this->myLocalVar, parent::__serialize()]; - } - - public function __unserialize(array $data): void - { - [$this->myLocalVar, $parentData] = $data; - parent::__unserialize($parentData); - } - ``` - - * The `Argon2iPasswordEncoder` class has been removed, use `SodiumPasswordEncoder` instead. - * The `BCryptPasswordEncoder` class has been removed, use `NativePasswordEncoder` instead. - * Classes implementing the `TokenInterface` must implement the two new methods - `__serialize` and `__unserialize` - * Implementations of `Guard\AuthenticatorInterface::checkCredentials()` must return a boolean value now. Please explicitly return `false` to indicate invalid credentials. - * Removed the `has_role()` function from security expressions, use `is_granted()` instead. - -SecurityBundle --------------- - - * The `logout_on_user_change` firewall option has been removed. - * The `switch_user.stateless` firewall option has been removed. - * The `SecurityUserValueResolver` class has been removed. - * Passing a `FirewallConfig` instance as 3rd argument to the `FirewallContext` constructor - now throws a `\TypeError`, pass a `LogoutListener` instance instead. - * The `security.authentication.trust_resolver.anonymous_class` parameter has been removed. - * The `security.authentication.trust_resolver.rememberme_class` parameter has been removed. - * The `simple_form` and `simple_preauth` authentication listeners have been removed, - use Guard instead. - * The `SimpleFormFactory` and `SimplePreAuthenticationFactory` classes have been removed, - use Guard instead. - * The names of the cookies configured in the `logout.delete_cookies` option are - no longer normalized. If any of your cookie names has dashes they won't be - changed to underscores. - Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore). - After: `my-cookie` deletes the `my-cookie` cookie (with a dash). - * Removed the `security.user.provider.in_memory.user` service. - -Serializer ----------- - - * The default value of the `CsvEncoder` "as_collection" option was changed to `true`. - * Individual encoders & normalizers options as constructor arguments were removed. - Use the default context instead. - * The following method and properties: - - `AbstractNormalizer::$circularReferenceLimit` - - `AbstractNormalizer::$circularReferenceHandler` - - `AbstractNormalizer::$callbacks` - - `AbstractNormalizer::$ignoredAttributes` - - `AbstractNormalizer::$camelizedAttributes` - - `AbstractNormalizer::setCircularReferenceLimit()` - - `AbstractNormalizer::setCircularReferenceHandler()` - - `AbstractNormalizer::setCallbacks()` - - `AbstractNormalizer::setIgnoredAttributes()` - - `AbstractObjectNormalizer::$maxDepthHandler` - - `AbstractObjectNormalizer::setMaxDepthHandler()` - - `XmlEncoder::setRootNodeName()` - - `XmlEncoder::getRootNodeName()` - - were removed, use the default context instead. - * The `AbstractNormalizer::handleCircularReference()` method has two new `$format` and `$context` arguments. - * Removed support for instantiating a `DataUriNormalizer` with a default MIME type guesser when the `symfony/mime` component isn't installed. - -Serializer ----------- - - * Removed the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant. Use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead. - -Stopwatch ---------- - - * Removed support for passing `null` as 1st (`$id`) argument of `Section::get()` method, pass a valid child section identifier instead. - -Translation ------------ - - * Support for using `null` as the locale in `Translator` has been removed. - * The `FileDumper::setBackup()` method has been removed. - * The `TranslationWriter::disableBackup()` method has been removed. - * The `TranslatorInterface` has been removed in favor of `Symfony\Contracts\Translation\TranslatorInterface` - * The `MessageSelector`, `Interval` and `PluralizationRules` classes have been removed, use `IdentityTranslator` instead - * The `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` method are now internal - * The `Translator::transChoice()` method has been removed in favor of using `Translator::trans()` with "%count%" as the parameter driving plurals - * Removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. - -TwigBundle ----------- - - * The default value (`false`) of the `twig.strict_variables` configuration option has been changed to `%kernel.debug%`. - * The `transchoice` tag and filter have been removed, use the `trans` ones instead with a `%count%` parameter. - * Removed support for legacy templates directories `src/Resources/views/` and `src/Resources//views/`, use `templates/` and `templates/bundles//` instead. - * The `twig.exception_controller` configuration option has been removed, use `framework.error_controller` instead. - * Removed `ExceptionController`, `PreviewErrorController` classes and all built-in error templates - -TwigBridge ----------- - - * Removed argument `$rootDir` from the `DebugCommand::__construct()` method and the 5th argument must be an instance of `FileLinkFormatter` - * removed the `$requestStack` and `$requestContext` arguments of the - `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` - instance as the only argument instead - * Removed support for implicit STDIN usage in the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. - -Validator --------- - - * Removed support for non-string codes of a `ConstraintViolation`. A `string` type-hint was added to the constructor of - the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()` method. - * An `ExpressionLanguage` instance or null must be passed as the first argument of `ExpressionValidator::__construct()` - * The `checkMX` and `checkHost` options of the `Email` constraint were removed - * The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead. - * Calling `EmailValidator::__construct()` method with a boolean parameter has been removed, use `EmailValidator("strict")` instead. - * Removed the `checkDNS` and `dnsMessage` options from the `Url` constraint. - * The component is now decoupled from `symfony/translation` and uses `Symfony\Contracts\Translation\TranslatorInterface` instead - * The `ValidatorBuilderInterface` has been removed - * Removed support for validating instances of `\DateTimeInterface` in `DateTimeValidator`, `DateValidator` and `TimeValidator`. Use `Type` instead or remove the constraint if the underlying model is type hinted to `\DateTimeInterface` already. - * The `symfony/intl` component is now required for using the `Bic`, `Country`, `Currency`, `Language` and `Locale` constraints - * The `egulias/email-validator` component is now required for using the `Email` constraint in strict mode - * The `symfony/expression-language` component is now required for using the `Expression` constraint - * Changed the default value of `Length::$allowEmptyString` to `false` and made it optional - * Added support for PHPUnit 8. A `void` return-type was added to the `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` methods. - * The `Symfony\Component\Validator\Mapping\Cache\CacheInterface` and all its implementations have been removed. - * The `ValidatorBuilder::setMetadataCache` has been removed, use `ValidatorBuilder::setMappingCache` instead. - -WebProfilerBundle ------------------ - - * Removed the `ExceptionController::templateExists()` method - * Removed the `TemplateManager::templateExists()` method - -Workflow --------- - - * The `DefinitionBuilder::reset()` method has been removed, use the `clear()` one instead. - * `add` method has been removed use `addWorkflow` method in `Workflow\Registry` instead. - * `SupportStrategyInterface` has been removed, use `WorkflowSupportStrategyInterface` instead. - * `ClassInstanceSupportStrategy` has been removed, use `InstanceOfSupportStrategy` instead. - * `WorkflowInterface::apply()` has a third argument: `array $context = []`. - * `MarkingStoreInterface::setMarking()` has a third argument: `array $context = []`. - * Removed support of `initial_place`. Use `initial_marking` instead. - * `MultipleStateMarkingStore` has been removed. Use `MethodMarkingStore` instead. - * `DefinitionBuilder::setInitialPlace()` has been removed, use `DefinitionBuilder::setInitialPlaces()` instead. - - Before: - ```yaml - framework: - workflows: - type: workflow - article: - marking_store: - type: multiple - arguments: states - ``` - - After: - ```yaml - framework: - workflows: - type: workflow - article: - marking_store: - property: states - ``` - * `SingleStateMarkingStore` has been removed. Use `MethodMarkingStore` instead. - - Before: - ```yaml - framework: - workflows: - article: - marking_store: - arguments: state - ``` - - After: - ```yaml - framework: - workflows: - article: - marking_store: - property: state - ``` - - * Support for using a workflow with a single state marking is dropped. Use a state machine instead. - - Before: - ```yaml - framework: - workflows: - article: - type: workflow - marking_store: - type: single_state - ``` - - After: - ```yaml - framework: - workflows: - article: - type: state_machine - ``` - -Yaml ----- - - * The parser is now stricter and will throw a `ParseException` when a - mapping is found inside a multi-line string. - * Removed support for implicit STDIN usage in the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit. - -WebProfilerBundle ------------------ - - * Removed the `ExceptionController` class, use `ExceptionErrorController` instead. - -WebServerBundle ---------------- - - * The bundle has been deprecated and can be installed separately. You may also use the Symfony Local Web Server instead. diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md new file mode 100644 index 0000000000000..e70e3e5d2a917 --- /dev/null +++ b/UPGRADE-6.0.md @@ -0,0 +1,559 @@ +UPGRADE FROM 5.x to 6.0 +======================= + +Asset +----- + + * Removed `RemoteJsonManifestVersionStrategy`, use `JsonManifestVersionStrategy` instead. + +DoctrineBridge +-------------- + + * Remove `UserLoaderInterface::loadUserByUsername()` in favor of `UserLoaderInterface::loadUserByIdentifier()` + * Add argument `$bundleDir` to `AbstractDoctrineExtension::getMappingDriverBundleConfigDefaults()` + * Add argument `$bundleDir` to `AbstractDoctrineExtension::getMappingResourceConfigDirectory()` + +Cache +----- + + * Remove `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package + * `PdoAdapter` does not accept `Doctrine\DBAL\Connection` or DBAL URL. Use the new `DoctrineDbalAdapter` instead + +Config +------ + + * The signature of method `NodeDefinition::setDeprecated()` has been updated to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `BaseNode::setDeprecated()` has been updated to `BaseNode::setDeprecation(string $package, string $version, string $message)`. + * Passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node is not supported anymore. + * Removed `BaseNode::getDeprecationMessage()`, use `BaseNode::getDeprecation()` instead. + +Console +------- + + * `Command::setHidden()` has a default value (`true`) for `$hidden` parameter + * Remove `Helper::strlen()`, use `Helper::width()` instead. + * Remove `Helper::strlenWithoutDecoration()`, use `Helper::removeDecoration()` instead. + * Remove `HelperSet::setCommand()` and `getCommand()` without replacement + +DependencyInjection +------------------- + + * The signature of method `Definition::setDeprecated()` has been updated to `Definition::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `Alias::setDeprecated()` has been updated to `Alias::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `DeprecateTrait::deprecate()` has been updated to `DeprecateTrait::deprecation(string $package, string $version, string $message)`. + * Removed the `Psr\Container\ContainerInterface` and `Symfony\Component\DependencyInjection\ContainerInterface` aliases of the `service_container` service, + configure them explicitly instead. + * Removed `Definition::getDeprecationMessage()`, use `Definition::getDeprecation()` instead. + * Removed `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead. + * The `inline()` function from the PHP-DSL has been removed, use `inline_service()` instead. + * The `ref()` function from the PHP-DSL has been removed, use `service()` instead. + * Removed `Definition::setPrivate()` and `Alias::setPrivate()`, use `setPublic()` instead + +DomCrawler +---------- + + * Removed the `parents()` method, use `ancestors()` instead. + +Dotenv +------ + + * Removed argument `$usePutenv` from Dotenv's constructor, use `Dotenv::usePutenv()` instead. + +EventDispatcher +--------------- + + * Removed `LegacyEventDispatcherProxy`. Use the event dispatcher without the proxy. + +Finder +------ + + * Remove `Comparator::setTarget()` and `Comparator::setOperator()` + * The `$target` parameter of `Comparator::__construct()` is now mandatory + +Form +---- + + * `FormErrorIterator::children()` throws an exception if the current element is not iterable. + * The default value of the `rounding_mode` option of the `PercentType` has been changed to `\NumberFormatter::ROUND_HALFUP`. + * The default rounding mode of the `PercentToLocalizedStringTransformer` has been changed to `\NumberFormatter::ROUND_HALFUP`. + * Added the `getIsEmptyCallback()` method to the `FormConfigInterface`. + * Added the `setIsEmptyCallback()` method to the `FormConfigBuilderInterface`. + * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`. + * The `Symfony\Component\Form\Extension\Validator\Util\ServerParams` class has been removed, use its parent `Symfony\Component\Form\Util\ServerParams` instead. + * The `NumberToLocalizedStringTransformer::ROUND_*` constants have been removed, use `\NumberFormatter::ROUND_*` instead. + * Removed `PropertyPathMapper` in favor of `DataMapper` and `PropertyPathAccessor`. + * Changed `$forms` parameter type of the `DataMapper::mapDataToForms()` method from `iterable` to `\Traversable`. + * Changed `$forms` parameter type of the `DataMapper::mapFormsToData()` method from `iterable` to `\Traversable`. + * Changed `$checkboxes` parameter type of the `CheckboxListMapper::mapDataToForms()` method from `iterable` to `\Traversable`. + * Changed `$checkboxes` parameter type of the `CheckboxListMapper::mapFormsToData()` method from `iterable` to `\Traversable`. + * Changed `$radios` parameter type of the `RadioListMapper::mapDataToForms()` method from `iterable` to `\Traversable`. + * Changed `$radios` parameter type of the `RadioListMapper::mapFormsToData()` method from `iterable` to `\Traversable`. + +FrameworkBundle +--------------- + + * Remove the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead + * Remove the `session.storage` alias and `session.storage.*` services, use the `session.storage.factory` alias and `session.storage.factory.*` services instead + * Remove `framework.session.storage_id` configuration option, use the `framework.session.storage_factory_id` configuration option instead + * Remove the `session` service and the `SessionInterface` alias, use the `\Symfony\Component\HttpFoundation\Request::getSession()` or the new `\Symfony\Component\HttpFoundation\RequestStack::getSession()` methods instead + * `MicroKernelTrait::configureRoutes()` is now always called with a `RoutingConfigurator` + * The "framework.router.utf8" configuration option defaults to `true` + * Removed `session.attribute_bag` service and `session.flash_bag` service. + * The `form.factory`, `form.type.file`, `profiler`, `translator`, `security.csrf.token_manager`, `serializer`, + `cache_clearer`, `filesystem` and `validator` services are now private. + * Removed the `lock.RESOURCE_NAME` and `lock.RESOURCE_NAME.store` services and the `lock`, `LockInterface`, `lock.store` and `PersistingStoreInterface` aliases, use `lock.RESOURCE_NAME.factory`, `lock.factory` or `LockFactory` instead. + * Remove the `KernelTestCase::$container` property, use `KernelTestCase::getContainer()` instead + * Registered workflow services are now private + * Remove option `--xliff-version` of the `translation:update` command, use e.g. `--output-format=xlf20` instead + * Remove option `--output-format` of the `translation:update` command, use e.g. `--output-format=xlf20` instead + * Remove the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead + * Remove `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead + * Deprecate the `cache.adapter.doctrine` service: The Doctrine Cache library is deprecated. Either switch to Symfony Cache or use the PSR-6 adapters provided by Doctrine Cache. + * Make the `framework.messenger.reset_on_message` configuration option default to `true` + * In `framework.cache` configuration, using the `cache.adapter.pdo` with a Doctrine DBAL connection is no longer supported, use `cache.adapter.doctrine_dbal` instead. + +HttpFoundation +-------------- + + * Remove the `NamespacedAttributeBag` class + * Removed `Response::create()`, `JsonResponse::create()`, + `RedirectResponse::create()`, `StreamedResponse::create()` and + `BinaryFileResponse::create()` methods (use `__construct()` instead) + * Not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead + * Not passing a `Closure` together with `FILTER_CALLBACK` to `InputBag::filter()` throws an `\InvalidArgumentException`; wrap your filter in a closure instead + * Removed the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead + * Rename `RequestStack::getMasterRequest()` to `getMainRequest()` + * Not passing `FILTER_REQUIRE_ARRAY` or `FILTER_FORCE_ARRAY` flags to `InputBag::filter()` when filtering an array will throw `BadRequestException` + * Retrieving non-scalar values using `InputBag::get()` will throw `BadRequestException` (use `InputBag::all()` instead to retrieve an array) + * Passing non-scalar default value as the second argument `InputBag::get()` will throw `\InvalidArgumentException` + * Passing non-scalar, non-array value as the second argument `InputBag::set()` will throw `\InvalidArgumentException` + * Passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()` is not supported anymore. + * Remove the `upload_progress.*` and `url_rewriter.tags` session options + +HttpKernel +---------- + + * Remove `ArgumentInterface` + * Remove `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead + * Make `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+ + * Remove support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead. + * Remove support for returning a `ContainerBuilder` from `KernelInterface::registerContainerConfiguration()` + * Rename `HttpKernelInterface::MASTER_REQUEST` to `MAIN_REQUEST` + * Rename `KernelEvent::isMasterRequest()` to `isMainRequest()` + +Inflector +--------- + + * The component has been removed, use `EnglishInflector` from the String component instead. + +Lock +---- + + * Removed the `NotSupportedException`. It shouldn't be thrown anymore. + * Removed the `RetryTillSaveStore`. Logic has been moved in `Lock` and is not needed anymore. + * Removed usage of `PdoStore` with a `Doctrine\DBAL\Connection` or a DBAL url, use the new `DoctrineDbalStore` instead + * Removed usage of `PostgreSqlStore` with a `Doctrine\DBAL\Connection` or a DBAL url, use the new `DoctrineDbalPostgreSqlStore` instead + +Mailer +------ + + * Removed the `SesApiTransport` class. Use `SesApiAsyncAwsTransport` instead. + * Removed the `SesHttpTransport` class. Use `SesHttpAsyncAwsTransport` instead. + +Messenger +--------- + + * Removed AmqpExt transport. Run `composer require symfony/amqp-messenger` to keep the transport in your application. + * Removed Doctrine transport. Run `composer require symfony/doctrine-messenger` to keep the transport in your application. + * Removed RedisExt transport. Run `composer require symfony/redis-messenger` to keep the transport in your application. + * Use of invalid options in Redis and AMQP connections now throws an error. + * The signature of method `RetryStrategyInterface::isRetryable()` has been updated to `RetryStrategyInterface::isRetryable(Envelope $message, \Throwable $throwable = null)`. + * The signature of method `RetryStrategyInterface::getWaitingTime()` has been updated to `RetryStrategyInterface::getWaitingTime(Envelope $message, \Throwable $throwable = null)`. + * Removed the `prefetch_count` parameter in the AMQP bridge. + * Removed the use of TLS option for Redis Bridge, use `rediss://127.0.0.1` instead of `redis://127.0.0.1?tls=1` + * The `delete_after_ack` config option of the Redis transport now defaults to `true` + +Mime +---- + + * Removed `Address::fromString()`, use `Address::create()` instead + +Monolog +------- + + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` has been replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` is now final. + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` has been replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` is now final. + * Remove `ResetLoggersWorkerSubscriber` in favor of "reset_on_message" option in messenger configuration + +Notifier +-------- + + * Remove `SlackOptions::channel()`, use `SlackOptions::recipient()` instead. + +OptionsResolver +--------------- + + * The signature of method `OptionsResolver::setDeprecated()` has been updated to `OptionsResolver::setDeprecated(string $option, string $package, string $version, $message)`. + * Removed `OptionsResolverIntrospector::getDeprecationMessage()`, use `OptionsResolverIntrospector::getDeprecation()` instead. + +PhpUnitBridge +------------- + + * Removed support for `@expectedDeprecation` annotations, use the `ExpectDeprecationTrait::expectDeprecation()` method instead. + * Removed the `SetUpTearDownTrait` trait, use original methods with "void" return typehint. + +PropertyAccess +-------------- + + * Drop support for booleans as the second argument of `PropertyAccessor::__construct()`, pass a combination of bitwise flags instead. + * Dropped support for booleans as the first argument of `PropertyAccessor::__construct()`. + Pass a combination of bitwise flags instead. + +PropertyInfo +------------ + + * Removed the `Type::getCollectionKeyType()` and `Type::getCollectionValueType()` methods, use `Type::getCollectionKeyTypes()` and `Type::getCollectionValueTypes()` instead. + * Dropped the `enable_magic_call_extraction` context option in `ReflectionExtractor::getWriteInfo()` and `ReflectionExtractor::getReadInfo()` in favor of `enable_magic_methods_extraction`. + +Routing +------- + + * Removed `RouteCollectionBuilder`. + * Added argument `$priority` to `RouteCollection::add()` + * Removed the `RouteCompiler::REGEX_DELIMITER` constant + * Removed the `$data` parameter from the constructor of the `Route` annotation class + +Security +-------- + + * Remove `AuthenticationEvents::AUTHENTICATION_FAILURE`, use the `LoginFailureEvent` instead + * Remove the `$authenticationEntryPoint` argument of `ChannelListener` + * Remove `RetryAuthenticationEntryPoint`, this code was inlined in the `ChannelListener` + * Remove `FormAuthenticationEntryPoint` and `BasicAuthenticationEntryPoint`, the `FormLoginAuthenticator` and `HttpBasicAuthenticator` should be used instead. + * Remove `AbstractRememberMeServices`, `PersistentTokenBasedRememberMeServices`, `RememberMeServicesInterface`, + `TokenBasedRememberMeServices`, use the remember me handler alternatives instead + * Remove `AnonymousToken` + * Remove `Token::getCredentials()`, tokens should no longer contain credentials (as they represent authenticated sessions) + * Restrict the return type of `Token::getUser()` to `UserInterface` (removing `string|\Stringable`) + * Remove `AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY` and `AuthenticatedVoter::IS_ANONYMOUS`, + use `AuthenticatedVoter::PUBLIC_ACCESS` instead. + + Before: + ```yaml + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + ``` + + After: + ```yaml + # config/packages/security.yaml + security: + # ... + access_control: + - { path: ^/login, roles: PUBLIC_ACCESS } + ``` + + * Remove `AuthenticationTrustResolverInterface::isAnonymous()` and the `is_anonymous()` expression function + as anonymous no longer exists in version 6, use the `isFullFledged()` or the new `isAuthenticated()` instead + if you want to check if the request is (fully) authenticated. + * Remove the 4th and 5th argument of `AuthorizationChecker` + * Remove the 5th argument of `AccessListener` + * Remove class `User`, use `InMemoryUser` or your own implementation instead. + If you are using the `isAccountNonLocked()`, `isAccountNonExpired()` or `isCredentialsNonExpired()` method, consider re-implementing them + in your own user class as they are not part of the `InMemoryUser` API + * Remove class `UserChecker`, use `InMemoryUserChecker` or your own implementation instead + * Remove `UserInterface::getPassword()` + If your `getPassword()` method does not return `null` (i.e. you are using password-based authentication), + you should implement `PasswordAuthenticatedUserInterface`. + + Before: + ```php + use Symfony\Component\Security\Core\User\UserInterface; + + class User implements UserInterface + { + // ... + + public function getPassword() + { + return $this->password; + } + } + ``` + + After: + ```php + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; + + class User implements UserInterface, PasswordAuthenticatedUserInterface + { + // ... + + public function getPassword(): ?string + { + return $this->password; + } + } + ``` + + * Remove `UserInterface::getSalt()` + If your `getSalt()` method does not return `null` (i.e. you are using password-based authentication with an old password hash algorithm that requires user-provided salts), + implement `LegacyPasswordAuthenticatedUserInterface`. + + Before: + ```php + use Symfony\Component\Security\Core\User\UserInterface; + + class User implements UserInterface + { + // ... + + public function getPassword() + { + return $this->password; + } + + public function getSalt() + { + return $this->salt; + } + } + ``` + + After: + ```php + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; + + class User implements UserInterface, LegacyPasswordAuthenticatedUserInterface + { + // ... + + public function getPassword(): ?string + { + return $this->password; + } + + public function getSalt(): ?string + { + return $this->salt; + } + } + ``` + + * Remove `UserInterface::getUsername()` in favor of `UserInterface::getUserIdentifier()` + * Remove `TokenInterface::getUsername()` in favor of `TokenInterface::getUserIdentifier()` + * Remove `UserProviderInterface::loadUserByUsername()` in favor of `UserProviderInterface::loadUserByIdentifier()` + * Remove `UsernameNotFoundException` in favor of `UserNotFoundException` and `getUsername()`/`setUsername()` in favor of `getUserIdentifier()`/`setUserIdentifier()` + * Remove `PersistentTokenInterface::getUsername()` in favor of `PersistentTokenInterface::getUserIdentifier()` + * Calling `PasswordUpgraderInterface::upgradePassword()` with a `UserInterface` instance that + does not implement `PasswordAuthenticatedUserInterface` now throws a `\TypeError`. + * Calling methods `hashPassword()`, `isPasswordValid()` and `needsRehash()` on `UserPasswordHasherInterface` + with a `UserInterface` instance that does not implement `PasswordAuthenticatedUserInterface` now throws a `\TypeError` + * Drop all classes in the `Core\Encoder\` sub-namespace, use the `PasswordHasher` component instead + * Drop support for `SessionInterface $session` as constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead + * Drop support for `session` provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead + * Make `SessionTokenStorage` throw a `SessionNotFoundException` when called outside a request context + * Removed `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute + * Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead. + * Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`. + * Added a `logout(Request $request, Response $response, TokenInterface $token)` method to the `RememberMeServicesInterface`. + * Removed `setProviderKey()`/`getProviderKey()` in favor of `setFirewallName()/getFirewallName()` + in `PreAuthenticatedToken`, `RememberMeToken`, `SwitchUserToken`, `UsernamePasswordToken`, + `DefaultAuthenticationSuccessHandler`. + * Removed the `AbstractRememberMeServices::$providerKey` property in favor of `AbstractRememberMeServices::$firewallName` + * `AccessDecisionManager` now throw an exception when a voter does not return a valid decision. + * Remove `AuthenticationManagerInterface`, `AuthenticationProviderManager`, `AnonymousAuthenticationProvider`, + `AuthenticationProviderInterface`, `DaoAuthenticationProvider`, `LdapBindAuthenticationProvider`, + `PreAuthenticatedAuthenticationProvider`, `RememberMeAuthenticationProvider`, `UserAuthenticationProvider` and + `AuthenticationFailureEvent` from security-core, use the new authenticator system instead + * Remove `AbstractAuthenticationListener`, `AbstractPreAuthenticatedListener`, `AnonymousAuthenticationListener`, + `BasicAuthenticationListener`, `RememberMeListener`, `RemoteUserAuthenticationListener`, + `UsernamePasswordFormAuthenticationListener`, `UsernamePasswordJsonAuthenticationListener` and `X509AuthenticationListener` + from security-http, use the new authenticator system instead + * Remove the Guard component, use the new authenticator system instead + * Remove `TokenInterface:isAuthenticated()` and `setAuthenticated()`, + return `null` from `getUser()` instead when a token is not authenticated + * Remove `DeauthenticatedEvent`, use `TokenDeauthenticatedEvent` instead + * Remove `CookieClearingLogoutHandler`, `SessionLogoutHandler` and `CsrfTokenClearingLogoutHandler`. + Use `CookieClearingLogoutListener`, `SessionLogoutListener` and `CsrfTokenClearingLogoutListener` instead + * Remove `AuthenticatorInterface::createAuthenticatedToken()`, use `AuthenticatorInterface::createToken()` instead + * Remove `PassportInterface`, `UserPassportInterface` and `PassportTrait`, use `Passport` instead. + Also, the return type declaration of `AuthenticatorInterface::authenticate()` was changed to `Passport` + + Before: + ```php + class MyAuthenticator implements AuthenticatorInterface + { + public function authenticate(Request $request): PassportInterface + { + } + } + ``` + + After: + ```php + class MyAuthenticator implements AuthenticatorInterface + { + public function authenticate(Request $request): Passport + { + } + } + ``` + * `AccessDecisionManager` does not accept strings as strategy anymore, + pass an instance of `AccessDecisionStrategyInterface` instead + * Removed the `$credentials` argument of `PreAuthenticatedToken`, + `SwitchUserToken` and `UsernamePasswordToken`: + + Before: + ```php + $token = new UsernamePasswordToken($user, $credentials, $firewallName, $roles); + $token = new PreAuthenticatedToken($user, $credentials, $firewallName, $roles); + $token = new SwitchUserToken($user, $credentials, $firewallName, $roles, $originalToken); + ``` + + After: + ```php + $token = new UsernamePasswordToken($user, $firewallName, $roles); + $token = new PreAuthenticatedToken($user, $firewallName, $roles); + $token = new SwitchUserToken($user, $firewallName, $roles, $originalToken); + ``` + +SecurityBundle +-------------- + + * Remove `FirewallConfig::getListeners()`, use `FirewallConfig::getAuthenticators()` instead + * Remove `security.authentication.basic_entry_point` and `security.authentication.retry_entry_point` services, + the logic is moved into the `HttpBasicAuthenticator` and `ChannelListener` respectively + * Remove `SecurityFactoryInterface` and `SecurityExtension::addSecurityListenerFactory()` in favor of + `AuthenticatorFactoryInterface` and `SecurityExtension::addAuthenticatorFactory()` + * Add `AuthenticatorFactoryInterface::getPriority()` which replaces `SecurityFactoryInterface::getPosition()`. + Previous positions are mapped to the following priorities: + + | Position | Constant | Priority | + | ----------- | ----------------------------------------------------- | -------- | + | pre_auth | `RemoteUserFactory::PRIORITY`/`X509Factory::PRIORITY` | -10 | + | form | `FormLoginFactory::PRIORITY` | -30 | + | http | `HttpBasicFactory::PRIORITY` | -50 | + | remember_me | `RememberMeFactory::PRIORITY` | -60 | + | anonymous | n/a | -70 | + + * Remove passing an array of arrays as 1st argument to `MainConfiguration`, pass a sorted flat array of + factories instead. + * Remove the `always_authenticate_before_granting` option + * Remove the `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command, + use `UserPasswordHashCommand` and `user:hash-password` instead + * Remove the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases, + use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead + * Remove the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases, + use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead + * The `security.authorization_checker` and `security.token_storage` services are now private + * Not setting the `enable_authenticator_manager` option to `true` now throws an exception + * Remove the `security.authentication.provider.*` services, use the new authenticator system instead + * Remove the `security.authentication.listener.*` services, use the new authenticator system instead + * Remove the Guard component integration, use the new authenticator system instead + * Remove the default provider for custom_authenticators when there is more than one registered provider + +Serializer +---------- + + * Removed `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead. + * `ArrayDenormalizer` does not implement `SerializerAwareInterface` anymore. + * The annotation classes cannot be constructed by passing an array of parameters as first argument anymore, use named arguments instead + +TwigBundle +---------- + + * The `twig` service is now private. + +Validator +--------- + + * Removed the `allowEmptyString` option from the `Length` constraint. + + Before: + + ```php + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\Length(min=5, allowEmptyString=true) + */ + ``` + + After: + + ```php + use Symfony\Component\Validator\Constraints as Assert; + + /** + * @Assert\AtLeastOneOf({ + * @Assert\Blank(), + * @Assert\Length(min=5) + * }) + */ + ``` + + * Removed the `NumberConstraintTrait` trait. + + * `ValidatorBuilder::enableAnnotationMapping()` does not accept a Doctrine annotation reader anymore. + + Before: + + ```php + $builder->enableAnnotationMapping($reader); + ``` + + After: + + ```php + $builder + ->enableAnnotationMapping() + ->setDoctrineAnnotationReader($reader); + ``` + + * `ValidatorBuilder::enableAnnotationMapping()` won't automatically setup a Doctrine annotation reader anymore. + + Before: + + ```php + $builder->enableAnnotationMapping(); + ``` + + After: + + ```php + $builder + ->enableAnnotationMapping() + ->addDefaultDoctrineAnnotationReader(); + ``` + +Workflow +-------- + + * Remove `InvalidTokenConfigurationException` + +Yaml +---- + + * Added support for parsing numbers prefixed with `0o` as octal numbers. + * Removed support for parsing numbers starting with `0` as octal numbers. They will be parsed as strings. Prefix numbers with `0o` + so that they are parsed as octal numbers. + + Before: + + ```yaml + Yaml::parse('072'); + ``` + + After: + + ```yaml + Yaml::parse('0o72'); + ``` + + * Removed support for using the `!php/object` and `!php/const` tags without a value. diff --git a/composer.json b/composer.json index 85fd3e01a2857..0c750c584e9ca 100644 --- a/composer.json +++ b/composer.json @@ -18,50 +18,50 @@ "provide": { "php-http/async-client-implementation": "*", "php-http/client-implementation": "*", - "psr/cache-implementation": "1.0|2.0", - "psr/container-implementation": "1.0", + "psr/cache-implementation": "2.0|3.0", + "psr/container-implementation": "1.1|2.0", "psr/event-dispatcher-implementation": "1.0", "psr/http-client-implementation": "1.0", - "psr/link-implementation": "1.0", - "psr/log-implementation": "1.0|2.0", - "psr/simple-cache-implementation": "1.0|2.0", - "symfony/cache-implementation": "1.0|2.0", - "symfony/event-dispatcher-implementation": "1.1", - "symfony/http-client-implementation": "1.1|2.0", - "symfony/service-implementation": "1.0|2.0", - "symfony/translation-implementation": "1.0|2.0" + "psr/link-implementation": "1.0|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0", + "symfony/event-dispatcher-implementation": "2.0|3.0", + "symfony/http-client-implementation": "3.0", + "symfony/service-implementation": "1.1|2.0|3.0", + "symfony/translation-implementation": "2.3|3.0" }, "require": { - "php": ">=7.1.3", + "php": ">=8.0.2", + "composer-runtime-api": ">=2.1", "ext-xml": "*", "friendsofphp/proxy-manager-lts": "^1.0.2", "doctrine/event-manager": "~1.0", - "doctrine/persistence": "^1.3|^2", - "twig/twig": "^1.43|^2.13|^3.0.4", - "psr/cache": "^1.0|^2.0", - "psr/container": "^1.0", - "psr/link": "^1.0", - "psr/log": "^1|^2", - "symfony/contracts": "^1.1.8", + "doctrine/persistence": "^2", + "twig/twig": "^2.13|^3.0.4", + "psr/cache": "^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/event-dispatcher": "^1.0", + "psr/link": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php72": "~1.5", - "symfony/polyfill-php73": "^1.11", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22" + "symfony/polyfill-php81": "^1.23", + "symfony/polyfill-uuid": "^1.15" }, "replace": { "symfony/asset": "self.version", - "symfony/amazon-mailer": "self.version", "symfony/browser-kit": "self.version", "symfony/cache": "self.version", "symfony/config": "self.version", "symfony/console": "self.version", "symfony/css-selector": "self.version", "symfony/dependency-injection": "self.version", - "symfony/debug": "self.version", "symfony/debug-bundle": "self.version", "symfony/doctrine-bridge": "self.version", "symfony/dom-crawler": "self.version", @@ -73,82 +73,89 @@ "symfony/finder": "self.version", "symfony/form": "self.version", "symfony/framework-bundle": "self.version", - "symfony/google-mailer": "self.version", "symfony/http-client": "self.version", "symfony/http-foundation": "self.version", "symfony/http-kernel": "self.version", - "symfony/inflector": "self.version", "symfony/intl": "self.version", "symfony/ldap": "self.version", "symfony/lock": "self.version", - "symfony/mailchimp-mailer": "self.version", "symfony/mailer": "self.version", - "symfony/mailgun-mailer": "self.version", "symfony/messenger": "self.version", "symfony/mime": "self.version", "symfony/monolog-bridge": "self.version", + "symfony/notifier": "self.version", "symfony/options-resolver": "self.version", - "symfony/postmark-mailer": "self.version", + "symfony/password-hasher": "self.version", "symfony/process": "self.version", "symfony/property-access": "self.version", "symfony/property-info": "self.version", "symfony/proxy-manager-bridge": "self.version", + "symfony/rate-limiter": "self.version", "symfony/routing": "self.version", - "symfony/security": "self.version", + "symfony/security-bundle": "self.version", "symfony/security-core": "self.version", "symfony/security-csrf": "self.version", "symfony/security-guard": "self.version", "symfony/security-http": "self.version", - "symfony/security-bundle": "self.version", - "symfony/sendgrid-mailer": "self.version", + "symfony/semaphore": "self.version", "symfony/serializer": "self.version", "symfony/stopwatch": "self.version", + "symfony/string": "self.version", "symfony/templating": "self.version", "symfony/translation": "self.version", "symfony/twig-bridge": "self.version", "symfony/twig-bundle": "self.version", + "symfony/uid": "self.version", "symfony/validator": "self.version", "symfony/var-dumper": "self.version", "symfony/var-exporter": "self.version", "symfony/web-link": "self.version", "symfony/web-profiler-bundle": "self.version", - "symfony/web-server-bundle": "self.version", "symfony/workflow": "self.version", "symfony/yaml": "self.version" }, "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "async-aws/ses": "^1.0", + "async-aws/sqs": "^1.0", + "async-aws/sns": "^1.0", "cache/integration-tests": "dev-master", - "composer/package-versions-deprecated": "^1.8", - "doctrine/annotations": "^1.10.4", - "doctrine/cache": "^1.6|^2.0", + "doctrine/annotations": "^1.13.1", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "^2.7|^3.0", - "doctrine/orm": "^2.6.3", + "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/orm": "^2.7.4", "guzzlehttp/promises": "^1.4", "masterminds/html5": "^2.6", - "monolog/monolog": "^1.25.1", + "monolog/monolog": "^1.25.1|^2", "nyholm/psr7": "^1.0", - "paragonie/sodium_compat": "^1.8", + "pda/pheanstalk": "^4.0", "php-http/httplug": "^1.0|^2.0", + "phpstan/phpdoc-parser": "^1.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", - "psr/simple-cache": "^1.0|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", "egulias/email-validator": "^2.1.10|^3.1", - "symfony/phpunit-bridge": "^5.2", + "symfony/mercure-bundle": "^0.3", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/runtime": "self.version", "symfony/security-acl": "~2.8|~3.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3" }, "conflict": { - "doctrine/dbal": "<2.7", + "ext-psr": "<1.1|>=2", + "async-aws/core": "<1.5", + "doctrine/annotations": "<1.13.1", + "doctrine/dbal": "<2.13.1", "egulias/email-validator": "~3.0.0", "masterminds/html5": "<2.6", - "monolog/monolog": ">=2", - "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", - "phpdocumentor/type-resolver": "<0.3.0|1.3.*", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.4.0", "ocramius/proxy-manager": "<2.1", "phpunit/phpunit": "<5.4.3" }, @@ -166,15 +173,17 @@ "Symfony\\Bundle\\": "src/Symfony/Bundle/", "Symfony\\Component\\": "src/Symfony/Component/" }, - "classmap": [ - "src/Symfony/Component/Intl/Resources/stubs" + "files": [ + "src/Symfony/Component/String/Resources/functions.php" ], "exclude-from-classmap": [ "**/Tests/" ] }, "autoload-dev": { - "files": [ "src/Symfony/Component/VarDumper/Resources/functions/dump.php" ] + "files": [ + "src/Symfony/Component/VarDumper/Resources/functions/dump.php" + ] }, "repositories": [ { @@ -182,9 +191,13 @@ "url": "src/Symfony/Contracts", "options": { "versions": { - "symfony/contracts": "1.1.x-dev" + "symfony/contracts": "3.0.x-dev" } } + }, + { + "type": "path", + "url": "src/Symfony/Component/Runtime" } ], "minimum-stability": "dev" diff --git a/link b/link index 60cd84dc4b569..29f9600d6b94e 100755 --- a/link +++ b/link @@ -41,13 +41,12 @@ if (!is_dir("$pathToProject/vendor/symfony")) { $sfPackages = array('symfony/symfony' => __DIR__); $filesystem = new Filesystem(); -$braces = array('Bundle', 'Bridge', 'Component', 'Component/Security', 'Component/Mailer/Bridge', 'Component/Messenger/Bridge', 'Component/Notifier/Bridge', 'Contracts'); +$braces = array('Bundle', 'Bridge', 'Component', 'Component/Security', 'Component/Mailer/Bridge', 'Component/Messenger/Bridge', 'Component/Notifier/Bridge', 'Contracts', 'Component/Translation/Bridge'); $directories = array_merge(...array_values(array_map(function ($part) { return glob(__DIR__.'/src/Symfony/'.$part.'/*', GLOB_ONLYDIR | GLOB_NOSORT); }, $braces))); $directories[] = __DIR__.'/src/Symfony/Contracts'; - foreach ($directories as $dir) { if ($filesystem->exists($composer = "$dir/composer.json")) { $sfPackages[json_decode(file_get_contents($composer))->name] = $dir; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cd85992d44d55..5ad6175de7a31 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ + + + @@ -43,26 +46,26 @@ - - + + ./src/Symfony/ - - ./src/Symfony/Bridge/*/Tests - ./src/Symfony/Component/*/Tests - ./src/Symfony/Component/*/*/Tests - ./src/Symfony/Contract/*/Tests - ./src/Symfony/Bundle/*/Tests - ./src/Symfony/Bundle/*/Resources - ./src/Symfony/Component/*/Resources - ./src/Symfony/Component/*/*/Resources - ./src/Symfony/Bridge/*/vendor - ./src/Symfony/Bundle/*/vendor - ./src/Symfony/Component/*/vendor - ./src/Symfony/Component/*/*/vendor - ./src/Symfony/Contract/*/vendor - - - + + + ./src/Symfony/Bridge/*/Tests + ./src/Symfony/Component/*/Tests + ./src/Symfony/Component/*/*/Tests + ./src/Symfony/Contract/*/Tests + ./src/Symfony/Bundle/*/Tests + ./src/Symfony/Bundle/*/Resources + ./src/Symfony/Component/*/Resources + ./src/Symfony/Component/*/*/Resources + ./src/Symfony/Bridge/*/vendor + ./src/Symfony/Bundle/*/vendor + ./src/Symfony/Component/*/vendor + ./src/Symfony/Component/*/*/vendor + ./src/Symfony/Contract/*/vendor + + @@ -71,13 +74,13 @@ Cache\IntegrationTests - Doctrine\Common\Cache - Symfony\Component\Cache - Symfony\Component\Cache\Tests\Fixtures - Symfony\Component\Cache\Tests\Traits - Symfony\Component\Cache\Traits - Symfony\Component\Console - Symfony\Component\HttpFoundation + Symfony\Component\Cache + Symfony\Component\Cache\Tests\Fixtures + Symfony\Component\Cache\Tests\Traits + Symfony\Component\Cache\Traits + Symfony\Component\Console + Symfony\Component\HttpFoundation + Symfony\Component\Uid diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 8fe8d410797fa..7c3e2c010b2a5 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,44 @@ CHANGELOG ========= +6.0 +--- + + * Remove `DoctrineTestHelper` and `TestRepositoryFactory` + +5.4 +--- + + * Add `DoctrineOpenTransactionLoggerMiddleware` to log when a transaction has been left open + * Deprecate `PdoCacheAdapterDoctrineSchemaSubscriber` and add `DoctrineDbalCacheAdapterSchemaSubscriber` instead + * `UniqueEntity` constraint retrieves a maximum of two entities if the default repository method is used. + * Add support for the newer bundle structure to `AbstractDoctrineExtension::loadMappingInformation()` + * Add argument `$bundleDir` to `AbstractDoctrineExtension::getMappingDriverBundleConfigDefaults()` + * Add argument `$bundleDir` to `AbstractDoctrineExtension::getMappingResourceConfigDirectory()` + +5.3 +--- + + * Deprecate `UserLoaderInterface::loadUserByUsername()` in favor of `UserLoaderInterface::loadUserByIdentifier() + * Deprecate `DoctrineTestHelper` and `TestRepositoryFactory` + * [BC BREAK] Remove `UuidV*Generator` classes + * Add `UuidGenerator` + * Add support for the new security-core `TokenVerifierInterface` in `DoctrineTokenProvider`, fixing parallel requests handling in remember-me + +5.2.0 +----- + + * added support for symfony/uid as `UlidType` and `UuidType` as Doctrine types + * added `UlidGenerator`, `UuidV1Generator`, `UuidV4Generator` and `UuidV6Generator` + +5.0.0 +----- + + * the `getMetadataDriverClass()` method is abstract and must be implemented by class extending `AbstractDoctrineExtension` + * passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field, throws an exception; pass `null` instead + * not explicitly passing an instance of `IdReader` to `DoctrineChoiceLoader` when it can optimize single id field, will not apply any optimization + * `DoctrineExtractor` now requires an `EntityManagerInterface` on instantiation + 4.4.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php index 24cf25be59f3d..e90c82af15b4a 100644 --- a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php +++ b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php @@ -33,19 +33,20 @@ public function __construct(ManagerRegistry $registry) /** * This cache warmer is not optional, without proxies fatal error occurs! - * - * @return false */ - public function isOptional() + public function isOptional(): bool { return false; } /** * {@inheritdoc} + * + * @return string[] A list of files to preload on PHP 7.4+ */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir): array { + $files = []; foreach ($this->registry->getManagers() as $em) { // we need the directory no matter the proxy cache generation strategy if (!is_dir($proxyCacheDir = $em->getConfiguration()->getProxyDir())) { @@ -64,6 +65,14 @@ public function warmUp($cacheDir) $classes = $em->getMetadataFactory()->getAllMetadata(); $em->getProxyFactory()->generateProxyClasses($classes); + + foreach (scandir($proxyCacheDir) as $file) { + if (!is_dir($file = $proxyCacheDir.'/'.$file)) { + $files[] = $file; + } + } } + + return $files; } } diff --git a/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php b/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php index 48385c93ac19e..0b979dbea3d47 100644 --- a/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php +++ b/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php @@ -13,6 +13,7 @@ use Doctrine\Common\EventArgs; use Doctrine\Common\EventManager; +use Doctrine\Common\EventSubscriber; use Psr\Container\ContainerInterface; /** @@ -27,13 +28,16 @@ class ContainerAwareEventManager extends EventManager * * => */ - private $listeners = []; - private $subscribers; - private $initialized = []; - private $initializedSubscribers = false; - private $methods = []; + private array $listeners = []; + private array $subscribers; + private array $initialized = []; + private bool $initializedSubscribers = false; + private array $methods = []; private $container; + /** + * @param list $subscriberIds List of subscribers, subscriber ids, or [events, listener] tuples + */ public function __construct(ContainerInterface $container, array $subscriberIds = []) { $this->container = $container; @@ -42,10 +46,8 @@ public function __construct(ContainerInterface $container, array $subscriberIds /** * {@inheritdoc} - * - * @return void */ - public function dispatchEvent($eventName, EventArgs $eventArgs = null) + public function dispatchEvent($eventName, EventArgs $eventArgs = null): void { if (!$this->initializedSubscribers) { $this->initializeSubscribers(); @@ -70,7 +72,7 @@ public function dispatchEvent($eventName, EventArgs $eventArgs = null) * * @return object[][] */ - public function getListeners($event = null) + public function getListeners($event = null): array { if (!$this->initializedSubscribers) { $this->initializeSubscribers(); @@ -94,10 +96,8 @@ public function getListeners($event = null) /** * {@inheritdoc} - * - * @return bool */ - public function hasListeners($event) + public function hasListeners($event): bool { if (!$this->initializedSubscribers) { $this->initializeSubscribers(); @@ -108,11 +108,13 @@ public function hasListeners($event) /** * {@inheritdoc} - * - * @return void */ - public function addEventListener($events, $listener) + public function addEventListener($events, $listener): void { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } + $hash = $this->getHash($listener); foreach ((array) $events as $event) { @@ -130,11 +132,13 @@ public function addEventListener($events, $listener) /** * {@inheritdoc} - * - * @return void */ - public function removeEventListener($events, $listener) + public function removeEventListener($events, $listener): void { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } + $hash = $this->getHash($listener); foreach ((array) $events as $event) { @@ -149,6 +153,24 @@ public function removeEventListener($events, $listener) } } + public function addEventSubscriber(EventSubscriber $subscriber): void + { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } + + parent::addEventSubscriber($subscriber); + } + + public function removeEventSubscriber(EventSubscriber $subscriber): void + { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } + + parent::removeEventSubscriber($subscriber); + } + private function initializeListeners(string $eventName) { $this->initialized[$eventName] = true; @@ -164,29 +186,20 @@ private function initializeListeners(string $eventName) private function initializeSubscribers() { $this->initializedSubscribers = true; - - $eventListeners = $this->listeners; - // reset eventListener to respect priority: EventSubscribers have a higher priority - $this->listeners = []; - foreach ($this->subscribers as $id => $subscriber) { - if (\is_string($subscriber)) { - parent::addEventSubscriber($this->subscribers[$id] = $this->container->get($subscriber)); + foreach ($this->subscribers as $subscriber) { + if (\is_array($subscriber)) { + $this->addEventListener(...$subscriber); + continue; } - } - foreach ($eventListeners as $event => $listeners) { - if (!isset($this->listeners[$event])) { - $this->listeners[$event] = []; + if (\is_string($subscriber)) { + $subscriber = $this->container->get($subscriber); } - unset($this->initialized[$event]); - $this->listeners[$event] += $listeners; + parent::addEventSubscriber($subscriber); } $this->subscribers = []; } - /** - * @param string|object $listener - */ - private function getHash($listener): string + private function getHash(string|object $listener): string { if (\is_string($listener)) { return '_service_'.$listener; @@ -195,10 +208,7 @@ private function getHash($listener): string return spl_object_hash($listener); } - /** - * @param object $listener - */ - private function getMethod($listener, string $event): string + private function getMethod(object $listener, string $event): string { if (!method_exists($listener, $event) && method_exists($listener, '__invoke')) { return '__invoke'; diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index d259e4123976a..6c5194897dac4 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -29,13 +29,13 @@ class DoctrineDataCollector extends DataCollector { private $registry; - private $connections; - private $managers; + private array $connections; + private array $managers; /** - * @var DebugStack[] + * @var array */ - private $loggers = []; + private array $loggers = []; public function __construct(ManagerRegistry $registry) { @@ -46,20 +46,16 @@ public function __construct(ManagerRegistry $registry) /** * Adds the stack logger for a connection. - * - * @param string $name */ - public function addLogger($name, DebugStack $logger) + public function addLogger(string $name, DebugStack $logger) { $this->loggers[$name] = $logger; } /** * {@inheritdoc} - * - * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response/*, \Throwable $exception = null*/) + public function collect(Request $request, Response $response, \Throwable $exception = null) { $queries = []; foreach ($this->loggers as $name => $logger) { @@ -118,7 +114,7 @@ public function getTime() /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'db'; } @@ -126,7 +122,7 @@ public function getName() /** * {@inheritdoc} */ - protected function getCasters() + protected function getCasters(): array { return parent::getCasters() + [ ObjectParameter::class => static function (ObjectParameter $o, array $a, Stub $s): array { @@ -217,7 +213,7 @@ private function sanitizeQuery(string $connectionName, array $query): array * indicating if the original value was kept (allowing to use the sanitized * value to explain the query). */ - private function sanitizeParam($var, ?\Throwable $error): array + private function sanitizeParam(mixed $var, ?\Throwable $error): array { if (\is_object($var)) { return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error]; diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php index 26bdb7ff267d0..549a6af8bb42a 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php @@ -13,15 +13,12 @@ final class ObjectParameter { - private $object; - private $error; - private $stringable; - private $class; - - /** - * @param object $object - */ - public function __construct($object, ?\Throwable $error) + private object $object; + private ?\Throwable $error; + private bool $stringable; + private string $class; + + public function __construct(object $object, ?\Throwable $error) { $this->object = $object; $this->error = $error; @@ -29,10 +26,7 @@ public function __construct($object, ?\Throwable $error) $this->class = \get_class($object); } - /** - * @return object - */ - public function getObject() + public function getObject(): object { return $this->object; } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index 383462ca95ee7..fc8910eb93f6d 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -73,9 +73,11 @@ protected function loadMappingInformation(array $objectManager, ContainerBuilder if ($mappingConfig['is_bundle']) { $bundle = null; + $bundleMetadata = null; foreach ($container->getParameter('kernel.bundles') as $name => $class) { if ($mappingName === $name) { $bundle = new \ReflectionClass($class); + $bundleMetadata = $container->getParameter('kernel.bundles_metadata')[$name]; break; } @@ -85,29 +87,12 @@ protected function loadMappingInformation(array $objectManager, ContainerBuilder throw new \InvalidArgumentException(sprintf('Bundle "%s" does not exist or it is not enabled.', $mappingName)); } - $mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container); + $mappingConfig = $this->getMappingDriverBundleConfigDefaults($mappingConfig, $bundle, $container, $bundleMetadata['path']); if (!$mappingConfig) { continue; } - } elseif (!$mappingConfig['type'] && \PHP_VERSION_ID < 80000) { - $mappingConfig['type'] = 'annotation'; } elseif (!$mappingConfig['type']) { - $mappingConfig['type'] = 'attribute'; - - $glob = new GlobResource($mappingConfig['dir'], '*', true); - $container->addResource($glob); - - foreach ($glob as $file) { - $content = file_get_contents($file); - - if (preg_match('/^#\[.*Entity\b/m', $content)) { - break; - } - if (preg_match('/^ \* @.*Entity\b/m', $content)) { - $mappingConfig['type'] = 'annotation'; - break; - } - } + $mappingConfig['type'] = $this->detectMappingType($mappingConfig['dir'], $container); } $this->assertValidMappingConfiguration($mappingConfig, $objectManager['name']); @@ -120,11 +105,8 @@ protected function loadMappingInformation(array $objectManager, ContainerBuilder * Register the alias for this mapping driver. * * Aliases can be used in the Query languages of all the Doctrine object managers to simplify writing tasks. - * - * @param array $mappingConfig - * @param string $mappingName */ - protected function setMappingDriverAlias($mappingConfig, $mappingName) + protected function setMappingDriverAlias(array $mappingConfig, string $mappingName) { if (isset($mappingConfig['alias'])) { $this->aliasMap[$mappingConfig['alias']] = $mappingConfig['prefix']; @@ -136,11 +118,9 @@ protected function setMappingDriverAlias($mappingConfig, $mappingName) /** * Register the mapping driver configuration for later use with the object managers metadata driver chain. * - * @param string $mappingName - * * @throws \InvalidArgumentException */ - protected function setMappingDriverConfig(array $mappingConfig, $mappingName) + protected function setMappingDriverConfig(array $mappingConfig, string $mappingName) { $mappingDirectory = $mappingConfig['dir']; if (!is_dir($mappingDirectory)) { @@ -154,15 +134,18 @@ protected function setMappingDriverConfig(array $mappingConfig, $mappingName) * If this is a bundle controlled mapping all the missing information can be autodetected by this method. * * Returns false when autodetection failed, an array of the completed information otherwise. - * - * @return array|false */ - protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \ReflectionClass $bundle, ContainerBuilder $container) + protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \ReflectionClass $bundle, ContainerBuilder $container, string $bundleDir = null): array|false { - $bundleDir = \dirname($bundle->getFileName()); + $bundleClassDir = \dirname($bundle->getFileName()); + $bundleDir ??= $bundleClassDir; if (!$bundleConfig['type']) { $bundleConfig['type'] = $this->detectMetadataDriver($bundleDir, $container); + + if (!$bundleConfig['type'] && $bundleDir !== $bundleClassDir) { + $bundleConfig['type'] = $this->detectMetadataDriver($bundleClassDir, $container); + } } if (!$bundleConfig['type']) { @@ -172,9 +155,9 @@ protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \Re if (!$bundleConfig['dir']) { if (\in_array($bundleConfig['type'], ['annotation', 'staticphp', 'attribute'])) { - $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingObjectDefaultName(); + $bundleConfig['dir'] = $bundleClassDir.'/'.$this->getMappingObjectDefaultName(); } else { - $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingResourceConfigDirectory(); + $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingResourceConfigDirectory($bundleDir); } } else { $bundleConfig['dir'] = $bundleDir.'/'.$bundleConfig['dir']; @@ -189,10 +172,8 @@ protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \Re /** * Register all the collected mapping information with the object manager by registering the appropriate mapping drivers. - * - * @param array $objectManager */ - protected function registerMappingDrivers($objectManager, ContainerBuilder $container) + protected function registerMappingDrivers(array $objectManager, ContainerBuilder $container) { // configure metadata driver for each bundle based on the type of mapping files found if ($container->hasDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'))) { @@ -246,11 +227,9 @@ protected function registerMappingDrivers($objectManager, ContainerBuilder $cont /** * Assertion if the specified mapping information is valid. * - * @param string $objectManagerName - * * @throws \InvalidArgumentException */ - protected function assertValidMappingConfiguration(array $mappingConfig, $objectManagerName) + protected function assertValidMappingConfiguration(array $mappingConfig, string $objectManagerName) { if (!$mappingConfig['type'] || !$mappingConfig['dir'] || !$mappingConfig['prefix']) { throw new \InvalidArgumentException(sprintf('Mapping definitions for Doctrine manager "%s" require at least the "type", "dir" and "prefix" options.', $objectManagerName)); @@ -267,14 +246,10 @@ protected function assertValidMappingConfiguration(array $mappingConfig, $object /** * Detects what metadata driver to use for the supplied directory. - * - * @param string $dir A directory path - * - * @return string|null A metadata driver short name, if one can be detected */ - protected function detectMetadataDriver($dir, ContainerBuilder $container) + protected function detectMetadataDriver(string $dir, ContainerBuilder $container): ?string { - $configPath = $this->getMappingResourceConfigDirectory(); + $configPath = $this->getMappingResourceConfigDirectory($dir); $extension = $this->getMappingResourceExtension(); if (glob($dir.'/'.$configPath.'/*.'.$extension.'.xml', \GLOB_NOSORT)) { @@ -291,7 +266,11 @@ protected function detectMetadataDriver($dir, ContainerBuilder $container) } $container->fileExists($resource, false); - return $container->fileExists($dir.'/'.$this->getMappingObjectDefaultName(), false) ? 'annotation' : null; + if ($container->fileExists($dir.'/'.$this->getMappingObjectDefaultName(), false)) { + return $this->detectMappingType($dir, $container); + } + + return null; } $container->fileExists($dir.'/'.$configPath, false); @@ -299,14 +278,40 @@ protected function detectMetadataDriver($dir, ContainerBuilder $container) } /** - * Loads a configured object manager metadata, query or result cache driver. + * Detects what mapping type to use for the supplied directory. * - * @param array $objectManager A configured object manager - * @param string $cacheName + * @return string A mapping type 'attribute' or 'annotation' + */ + private function detectMappingType(string $directory, ContainerBuilder $container): string + { + $type = 'attribute'; + + $glob = new GlobResource($directory, '*', true); + $container->addResource($glob); + + $quotedMappingObjectName = preg_quote($this->getMappingObjectDefaultName(), '/'); + + foreach ($glob as $file) { + $content = file_get_contents($file); + + if (preg_match('/^#\[.*'.$quotedMappingObjectName.'\b/m', $content)) { + break; + } + if (preg_match('/^ \* @.*'.$quotedMappingObjectName.'\b/m', $content)) { + $type = 'annotation'; + break; + } + } + + return $type; + } + + /** + * Loads a configured object manager metadata, query or result cache driver. * * @throws \InvalidArgumentException in case of unknown driver type */ - protected function loadObjectManagerCacheDriver(array $objectManager, ContainerBuilder $container, $cacheName) + protected function loadObjectManagerCacheDriver(array $objectManager, ContainerBuilder $container, string $cacheName) { $this->loadCacheDriver($cacheName, $objectManager['name'], $objectManager[$cacheName.'_driver'], $container); } @@ -314,15 +319,9 @@ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerB /** * Loads a cache driver. * - * @param string $cacheName The cache driver name - * @param string $objectManagerName The object manager name - * @param array $cacheDriver The cache driver mapping - * - * @return string - * * @throws \InvalidArgumentException */ - protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheDriver, ContainerBuilder $container) + protected function loadCacheDriver(string $cacheName, string $objectManagerName, array $cacheDriver, ContainerBuilder $container): string { $cacheDriverServiceId = $this->getObjectManagerElementName($objectManagerName.'_'.$cacheName); @@ -338,7 +337,6 @@ protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheD $memcachedPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.memcached_port').'%'; $cacheDef = new Definition($memcachedClass); $memcachedInstance = new Definition($memcachedInstanceClass); - $memcachedInstance->setPrivate(true); $memcachedInstance->addMethodCall('addServer', [ $memcachedHost, $memcachedPort, ]); @@ -352,7 +350,6 @@ protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheD $redisPort = !empty($cacheDriver['port']) ? $cacheDriver['port'] : '%'.$this->getObjectManagerElementName('cache.redis_port').'%'; $cacheDef = new Definition($redisClass); $redisInstance = new Definition($redisInstanceClass); - $redisInstance->setPrivate(true); $redisInstance->addMethodCall('connect', [ $redisHost, $redisPort, ]); @@ -376,11 +373,12 @@ protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheD if (!isset($cacheDriver['namespace'])) { // generate a unique namespace for the given application if ($container->hasParameter('cache.prefix.seed')) { - $seed = '.'.$container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); + $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); } else { $seed = '_'.$container->getParameter('kernel.project_dir'); + $seed .= '.'.$container->getParameter('kernel.container_class'); } - $seed .= '.'.$container->getParameter('kernel.container_class'); + $namespace = 'sf_'.$this->getMappingResourceExtension().'_'.$objectManagerName.'_'.ContainerBuilder::hash($seed); $cacheDriver['namespace'] = $namespace; @@ -397,10 +395,8 @@ protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheD * Returns a modified version of $managerConfigs. * * The manager called $autoMappedManager will map all bundles that are not mapped by other managers. - * - * @return array The modified version of $managerConfigs */ - protected function fixManagersAutoMappings(array $managerConfigs, array $bundles) + protected function fixManagersAutoMappings(array $managerConfigs, array $bundles): array { if ($autoMappedManager = $this->validateAutoMapping($managerConfigs)) { foreach (array_keys($bundles) as $bundle) { @@ -424,45 +420,30 @@ protected function fixManagersAutoMappings(array $managerConfigs, array $bundles * Prefixes the relative dependency injection container path with the object manager prefix. * * @example $name is 'entity_manager' then the result would be 'doctrine.orm.entity_manager' - * - * @param string $name - * - * @return string */ - abstract protected function getObjectManagerElementName($name); + abstract protected function getObjectManagerElementName(string $name): string; /** * Noun that describes the mapped objects such as Entity or Document. * * Will be used for autodetection of persistent objects directory. - * - * @return string */ - abstract protected function getMappingObjectDefaultName(); + abstract protected function getMappingObjectDefaultName(): string; /** * Relative path from the bundle root to the directory where mapping files reside. - * - * @return string */ - abstract protected function getMappingResourceConfigDirectory(); + abstract protected function getMappingResourceConfigDirectory(string $bundleDir = null): string; /** * Extension used by the mapping files. - * - * @return string */ - abstract protected function getMappingResourceExtension(); + abstract protected function getMappingResourceExtension(): string; /** * The class name used by the various mapping drivers. */ - protected function getMetadataDriverClass(string $driverType): string - { - @trigger_error(sprintf('Not declaring the "%s" method in class "%s" is deprecated since Symfony 4.4. This method will be abstract in Symfony 5.0.', __METHOD__, static::class), \E_USER_DEPRECATED); - - return '%'.$this->getObjectManagerElementName('metadata.'.$driverType.'.class').'%'; - } + abstract protected function getMetadataDriverClass(string $driverType): string; /** * Search for a manager that is declared as 'auto_mapping' = true. diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php index 25776641796fe..92985d89ca4ca 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php @@ -21,7 +21,7 @@ */ class DoctrineValidationPass implements CompilerPassInterface { - private $managerType; + private string $managerType; public function __construct(string $managerType) { diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php index 61046c28a5098..74a3d3200f05b 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; @@ -29,20 +30,25 @@ */ class RegisterEventListenersAndSubscribersPass implements CompilerPassInterface { - private $connections; - private $eventManagers; - private $managerTemplate; - private $tagPrefix; + private string $connectionsParameter; + private array $connections; + + /** + * @var array + */ + private array $eventManagers = []; + + private string $managerTemplate; + private string $tagPrefix; /** - * @param string $connections Parameter ID for connections * @param string $managerTemplate sprintf() template for generating the event * manager's service ID for a connection name * @param string $tagPrefix Tag prefix for listeners and subscribers */ - public function __construct(string $connections, string $managerTemplate, string $tagPrefix) + public function __construct(string $connectionsParameter, string $managerTemplate, string $tagPrefix) { - $this->connections = $connections; + $this->connectionsParameter = $connectionsParameter; $this->managerTemplate = $managerTemplate; $this->tagPrefix = $tagPrefix; } @@ -52,14 +58,12 @@ public function __construct(string $connections, string $managerTemplate, string */ public function process(ContainerBuilder $container) { - if (!$container->hasParameter($this->connections)) { + if (!$container->hasParameter($this->connectionsParameter)) { return; } - $this->connections = $container->getParameter($this->connections); - $listenerRefs = []; - $this->addTaggedSubscribers($container, $listenerRefs); - $this->addTaggedListeners($container, $listenerRefs); + $this->connections = $container->getParameter($this->connectionsParameter); + $listenerRefs = $this->addTaggedServices($container); // replace service container argument of event managers with smaller service locator // so services can even remain private @@ -69,15 +73,22 @@ public function process(ContainerBuilder $container) } } - private function addTaggedSubscribers(ContainerBuilder $container, array &$listenerRefs) + private function addTaggedServices(ContainerBuilder $container): array { + $listenerTag = $this->tagPrefix.'.event_listener'; $subscriberTag = $this->tagPrefix.'.event_subscriber'; - $taggedSubscribers = $this->findAndSortTags($subscriberTag, $container); + $listenerRefs = []; + $taggedServices = $this->findAndSortTags([$subscriberTag, $listenerTag], $container); $managerDefs = []; - foreach ($taggedSubscribers as $taggedSubscriber) { - [$id, $tag] = $taggedSubscriber; - $connections = isset($tag['connection']) ? [$tag['connection']] : array_keys($this->connections); + foreach ($taggedServices as $taggedSubscriber) { + [$tagName, $id, $tag] = $taggedSubscriber; + $connections = isset($tag['connection']) + ? [$container->getParameterBag()->resolveValue($tag['connection'])] + : array_keys($this->connections); + if ($listenerTag === $tagName && !isset($tag['event'])) { + throw new InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id)); + } foreach ($connections as $con) { if (!isset($this->connections[$con])) { throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: "%s".', $con, $id, implode('", "', array_keys($this->connections)))); @@ -95,39 +106,25 @@ private function addTaggedSubscribers(ContainerBuilder $container, array &$liste } if (ContainerAwareEventManager::class === $managerClass) { - $listenerRefs[$con][$id] = new Reference($id); $refs = $managerDef->getArguments()[1] ?? []; - $refs[] = $id; + $listenerRefs[$con][$id] = new Reference($id); + if ($subscriberTag === $tagName) { + $refs[] = $id; + } else { + $refs[] = [[$tag['event']], $id]; + } $managerDef->setArgument(1, $refs); } else { - $managerDef->addMethodCall('addEventSubscriber', [new Reference($id)]); + if ($subscriberTag === $tagName) { + $managerDef->addMethodCall('addEventSubscriber', [new Reference($id)]); + } else { + $managerDef->addMethodCall('addEventListener', [[$tag['event']], new Reference($id)]); + } } } } - } - private function addTaggedListeners(ContainerBuilder $container, array &$listenerRefs) - { - $listenerTag = $this->tagPrefix.'.event_listener'; - $taggedListeners = $this->findAndSortTags($listenerTag, $container); - - foreach ($taggedListeners as $taggedListener) { - [$id, $tag] = $taggedListener; - if (!isset($tag['event'])) { - throw new InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id)); - } - - $connections = isset($tag['connection']) ? [$tag['connection']] : array_keys($this->connections); - foreach ($connections as $con) { - if (!isset($this->connections[$con])) { - throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: "%s".', $con, $id, implode('", "', array_keys($this->connections)))); - } - $listenerRefs[$con][$id] = new Reference($id); - - // we add one call per event per service so we have the correct order - $this->getEventManagerDef($container, $con)->addMethodCall('addEventListener', [[$tag['event']], $id]); - } - } + return $listenerRefs; } private function getEventManagerDef(ContainerBuilder $container, string $name) @@ -149,14 +146,16 @@ private function getEventManagerDef(ContainerBuilder $container, string $name) * @see https://bugs.php.net/53710 * @see https://bugs.php.net/60926 */ - private function findAndSortTags(string $tagName, ContainerBuilder $container): array + private function findAndSortTags(array $tagNames, ContainerBuilder $container): array { $sortedTags = []; - foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $tags) { - foreach ($tags as $attributes) { - $priority = $attributes['priority'] ?? 0; - $sortedTags[$priority][] = [$serviceId, $attributes]; + foreach ($tagNames as $tagName) { + foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $tags) { + foreach ($tags as $attributes) { + $priority = $attributes['priority'] ?? 0; + $sortedTags[$priority][] = [$tagName, $serviceId, $attributes]; + } } } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php index e253720d8026f..0aeb25d287488 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php @@ -76,25 +76,21 @@ abstract class RegisterMappingsPass implements CompilerPassInterface /** * Naming pattern for the configuration service id, for example * 'doctrine.orm.%s_configuration'. - * - * @var string */ - private $configurationPattern; + private string $configurationPattern; /** * Method name to call on the configuration service. This depends on the * Doctrine implementation. For example addEntityNamespace. - * - * @var string */ - private $registerAliasMethodName; + private string $registerAliasMethodName; /** * Map of alias to namespace. * * @var string[] */ - private $aliasMap; + private array $aliasMap; /** * The $managerParameters is an ordered list of container parameters that could provide the @@ -117,7 +113,7 @@ abstract class RegisterMappingsPass implements CompilerPassInterface * register alias * @param string[] $aliasMap Map of alias to namespace */ - public function __construct($driver, array $namespaces, array $managerParameters, string $driverPattern, $enabledParameter = false, string $configurationPattern = '', string $registerAliasMethodName = '', array $aliasMap = []) + public function __construct(Definition|Reference $driver, array $namespaces, array $managerParameters, string $driverPattern, string|false $enabledParameter = false, string $configurationPattern = '', string $registerAliasMethodName = '', array $aliasMap = []) { $this->driver = $driver; $this->namespaces = $namespaces; @@ -165,12 +161,10 @@ public function process(ContainerBuilder $container) * Get the service name of the metadata chain driver that the mappings * should be registered with. * - * @return string The name of the chain driver service - * * @throws InvalidArgumentException if non of the managerParameters has a * non-empty value */ - protected function getChainDriverServiceName(ContainerBuilder $container) + protected function getChainDriverServiceName(ContainerBuilder $container): string { return sprintf($this->driverPattern, $this->getManagerName($container)); } @@ -180,10 +174,8 @@ protected function getChainDriverServiceName(ContainerBuilder $container) * * @param ContainerBuilder $container Passed on in case an extending class * needs access to the container - * - * @return Definition|Reference the metadata driver to add to all chain drivers */ - protected function getDriver(ContainerBuilder $container) + protected function getDriver(ContainerBuilder $container): Definition|Reference { return $this->driver; } @@ -227,10 +219,8 @@ private function getManagerName(ContainerBuilder $container): string * * This default implementation checks if the class has the enabledParameter * configured and if so if that parameter is present in the container. - * - * @return bool whether this compiler pass really should register the mappings */ - protected function enabled(ContainerBuilder $container) + protected function enabled(ContainerBuilder $container): bool { return !$this->enabledParameter || $container->hasParameter($this->enabledParameter); } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterUidTypePass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterUidTypePass.php new file mode 100644 index 0000000000000..95a74533ba0f5 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterUidTypePass.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass; + +use Symfony\Bridge\Doctrine\Types\UlidType; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Uid\AbstractUid; + +final class RegisterUidTypePass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!class_exists(AbstractUid::class)) { + return; + } + + if (!$container->hasParameter('doctrine.dbal.connection_factory.types')) { + return; + } + + $typeDefinition = $container->getParameter('doctrine.dbal.connection_factory.types'); + + if (!isset($typeDefinition['uuid'])) { + $typeDefinition['uuid'] = ['class' => UuidType::class]; + } + + if (!isset($typeDefinition['ulid'])) { + $typeDefinition['ulid'] = ['class' => UlidType::class]; + } + + $container->setParameter('doctrine.dbal.connection_factory.types', $typeDefinition); + } +} diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php index 352bf79bfbc7e..73d5a9d654d4f 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/Security/UserProvider/EntityFactory.php @@ -24,8 +24,8 @@ */ class EntityFactory implements UserProviderFactoryInterface { - private $key; - private $providerId; + private string $key; + private string $providerId; public function __construct(string $key, string $providerId) { @@ -33,7 +33,7 @@ public function __construct(string $key, string $providerId) $this->providerId = $providerId; } - public function create(ContainerBuilder $container, $id, $config) + public function create(ContainerBuilder $container, string $id, array $config) { $container ->setDefinition($id, new ChildDefinition($this->providerId)) @@ -52,7 +52,11 @@ public function addConfiguration(NodeDefinition $node) { $node ->children() - ->scalarNode('class')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('class') + ->isRequired() + ->info('The full entity class name of your user class.') + ->cannotBeEmpty() + ->end() ->scalarNode('property')->defaultNull()->end() ->scalarNode('manager_name')->defaultNull()->end() ->end() diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index c5ba42d471c35..9730690009b0f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -12,27 +12,21 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; use Doctrine\Persistence\ObjectManager; -use Symfony\Component\Form\ChoiceList\ArrayChoiceList; -use Symfony\Component\Form\ChoiceList\ChoiceListInterface; -use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader; +use Symfony\Component\Form\Exception\LogicException; /** * Loads choices using a Doctrine object manager. * * @author Bernhard Schussek */ -class DoctrineChoiceLoader implements ChoiceLoaderInterface +class DoctrineChoiceLoader extends AbstractChoiceLoader { private $manager; - private $class; + private string $class; private $idReader; private $objectLoader; - /** - * @var ChoiceListInterface - */ - private $choiceList; - /** * Creates a new choice loader. * @@ -47,20 +41,7 @@ public function __construct(ObjectManager $manager, string $class, IdReader $idR $classMetadata = $manager->getClassMetadata($class); if ($idReader && !$idReader->isSingleId()) { - @trigger_error(sprintf('Passing an instance of "%s" to "%s" with an entity class "%s" that has a composite id is deprecated since Symfony 4.3 and will throw an exception in 5.0.', IdReader::class, __CLASS__, $class), \E_USER_DEPRECATED); - - // In Symfony 5.0 - // throw new \InvalidArgumentException(sprintf('The second argument `$idReader` of "%s" must be null when the query cannot be optimized because of composite id fields.', __METHOD__)); - } - - if ((5 > \func_num_args() || false !== func_get_arg(4)) && null === $idReader) { - $idReader = new IdReader($manager, $classMetadata); - - if ($idReader->isSingleId()) { - @trigger_error(sprintf('Not explicitly passing an instance of "%s" to "%s" when it can optimize single id entity "%s" has been deprecated in 4.3 and will not apply any optimization in 5.0.', IdReader::class, __CLASS__, $class), \E_USER_DEPRECATED); - } else { - $idReader = null; - } + throw new \InvalidArgumentException(sprintf('The second argument `$idReader` of "%s" must be null when the query cannot be optimized because of composite id fields.', __METHOD__)); } $this->manager = $manager; @@ -72,81 +53,53 @@ public function __construct(ObjectManager $manager, string $class, IdReader $idR /** * {@inheritdoc} */ - public function loadChoiceList($value = null) + protected function loadChoices(): iterable { - if ($this->choiceList) { - return $this->choiceList; - } - - $objects = $this->objectLoader + return $this->objectLoader ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); - - return $this->choiceList = new ArrayChoiceList($objects, $value); } /** - * {@inheritdoc} + * @internal to be remove in Symfony 6 */ - public function loadValuesForChoices(array $choices, $value = null) + protected function doLoadValuesForChoices(array $choices): array { - // Performance optimization - if (empty($choices)) { - return []; - } - // Optimize performance for single-field identifiers. We already // know that the IDs are used as values - $optimize = $this->idReader && (null === $value || \is_array($value) && $value[0] === $this->idReader); - // Attention: This optimization does not check choices for existence - if ($optimize && !$this->choiceList && $this->idReader->isSingleId()) { - $values = []; - - // Maintain order and indices of the given objects - foreach ($choices as $i => $object) { - if ($object instanceof $this->class) { - // Make sure to convert to the right format - $values[$i] = (string) $this->idReader->getIdValue($object); - } - } - - return $values; + if ($this->idReader) { + throw new LogicException('Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.'); } - return $this->loadChoiceList($value)->getValuesForChoices($choices); + return parent::doLoadValuesForChoices($choices); } - /** - * {@inheritdoc} - */ - public function loadChoicesForValues(array $values, $value = null) + protected function doLoadChoicesForValues(array $values, ?callable $value): array { - // Performance optimization - // Also prevents the generation of "WHERE id IN ()" queries through the - // object loader. At least with MySQL and on the development machine - // this was tested on, no exception was thrown for such invalid - // statements, consequently no test fails when this code is removed. - // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 - if (empty($values)) { - return []; + if ($this->idReader && null === $value) { + throw new LogicException('Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.'); + } + + $idReader = null; + if (\is_array($value) && $value[0] instanceof IdReader) { + $idReader = $value[0]; + } elseif ($value instanceof \Closure && ($rThis = (new \ReflectionFunction($value))->getClosureThis()) instanceof IdReader) { + $idReader = $rThis; } // Optimize performance in case we have an object loader and // a single-field identifier - $optimize = $this->idReader && (null === $value || \is_array($value) && $this->idReader === $value[0]); - - if ($optimize && !$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { - $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); - $objectsById = []; + if ($idReader && $this->objectLoader) { $objects = []; + $objectsById = []; // Maintain order and indices from the given $values // An alternative approach to the following loop is to add the // "INDEX BY" clause to the Doctrine query in the loader, // but I'm not sure whether that's doable in a generic fashion. - foreach ($unorderedObjects as $object) { - $objectsById[(string) $this->idReader->getIdValue($object)] = $object; + foreach ($this->objectLoader->getEntitiesByIds($idReader->getIdField(), $values) as $object) { + $objectsById[$idReader->getIdValue($object)] = $object; } foreach ($values as $i => $id) { @@ -158,6 +111,6 @@ public function loadChoicesForValues(array $values, $value = null) return $objects; } - return $this->loadChoiceList($value)->getChoicesForValues($values); + return parent::doLoadChoicesForValues($values, $value); } } diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php index e36043af63cb8..51a497b44ca05 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityLoaderInterface.php @@ -20,20 +20,11 @@ interface EntityLoaderInterface { /** * Returns an array of entities that are valid choices in the corresponding choice list. - * - * @return array The entities */ - public function getEntities(); + public function getEntities(): array; /** * Returns an array of entities matching the given identifiers. - * - * @param string $identifier The identifier field of the object. This method - * is not applicable for fields with multiple - * identifiers. - * @param array $values The values of the identifiers - * - * @return array The entities */ - public function getEntitiesByIds($identifier, array $values); + public function getEntitiesByIds(string $identifier, array $values): array; } diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index f56193e81062f..35b2430f781cb 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -26,14 +26,10 @@ class IdReader { private $om; private $classMetadata; - private $singleId; - private $intId; - private $idField; - - /** - * @var IdReader|null - */ - private $associationIdReader; + private bool $singleId; + private bool $intId; + private string $idField; + private ?self $associationIdReader = null; public function __construct(ObjectManager $om, ClassMetadata $classMetadata) { @@ -59,9 +55,6 @@ public function __construct(ObjectManager $om, ClassMetadata $classMetadata) /** * Returns whether the class has a single-column ID. - * - * @return bool returns `true` if the class has a single-column ID and - * `false` otherwise */ public function isSingleId(): bool { @@ -70,9 +63,6 @@ public function isSingleId(): bool /** * Returns whether the class has a single-column integer ID. - * - * @return bool returns `true` if the class has a single-column integer ID - * and `false` otherwise */ public function isIntId(): bool { @@ -83,19 +73,15 @@ public function isIntId(): bool * Returns the ID value for an object. * * This method assumes that the object has a single-column ID. - * - * @param object $object The object - * - * @return mixed The ID value */ - public function getIdValue($object) + public function getIdValue(object $object = null): string { if (!$object) { - return null; + return ''; } if (!$this->om->contains($object)) { - throw new RuntimeException(sprintf('Entity of type "%s" passed to the choice field must be managed. Maybe you forget to persist it in the entity manager?', \get_class($object))); + throw new RuntimeException(sprintf('Entity of type "%s" passed to the choice field must be managed. Maybe you forget to persist it in the entity manager?', get_debug_type($object))); } $this->om->initializeObject($object); @@ -106,15 +92,13 @@ public function getIdValue($object) $idValue = $this->associationIdReader->getIdValue($idValue); } - return $idValue; + return (string) $idValue; } /** * Returns the name of the ID field. * * This method assumes that the object has a single-column ID. - * - * @return string The name of the ID field */ public function getIdField(): string { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index a96a543a60a12..5c74ad5ebabbb 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -12,7 +12,10 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\Exception\TransformationFailedException; /** * Loads entities using a {@link QueryBuilder} instance. @@ -27,8 +30,6 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface * entities. * * This property should only be accessed through queryBuilder. - * - * @var QueryBuilder */ private $queryBuilder; @@ -40,7 +41,7 @@ public function __construct(QueryBuilder $queryBuilder) /** * {@inheritdoc} */ - public function getEntities() + public function getEntities(): array { return $this->queryBuilder->getQuery()->execute(); } @@ -48,7 +49,7 @@ public function getEntities() /** * {@inheritdoc} */ - public function getEntitiesByIds($identifier, array $values) + public function getEntitiesByIds(string $identifier, array $values): array { if (null !== $this->queryBuilder->getMaxResults() || null !== $this->queryBuilder->getFirstResult()) { // an offset or a limit would apply on results including the where clause with submitted id values @@ -74,7 +75,7 @@ public function getEntitiesByIds($identifier, array $values) // Guess type $entity = current($qb->getRootEntities()); $metadata = $qb->getEntityManager()->getClassMetadata($entity); - if (\in_array($metadata->getTypeOfField($identifier), ['integer', 'bigint', 'smallint'])) { + if (\in_array($type = $metadata->getTypeOfField($identifier), ['integer', 'bigint', 'smallint'])) { $parameterType = Connection::PARAM_INT_ARRAY; // Filter out non-integer values (e.g. ""). If we don't, some @@ -82,13 +83,27 @@ public function getEntitiesByIds($identifier, array $values) $values = array_values(array_filter($values, function ($v) { return (string) $v === (string) (int) $v || ctype_digit($v); })); - } elseif (\in_array($metadata->getTypeOfField($identifier), ['uuid', 'guid'])) { + } elseif (\in_array($type, ['ulid', 'uuid', 'guid'])) { $parameterType = Connection::PARAM_STR_ARRAY; // Like above, but we just filter out empty strings. $values = array_values(array_filter($values, function ($v) { return '' !== (string) $v; })); + + // Convert values into right type + if (Type::hasType($type)) { + $doctrineType = Type::getType($type); + $platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform(); + foreach ($values as &$value) { + try { + $value = $doctrineType->convertToDatabaseValue($value, $platform); + } catch (ConversionException $e) { + throw new TransformationFailedException(sprintf('Failed to transform "%s" into "%s".', $value, $type), 0, $e); + } + } + unset($value); + } } else { $parameterType = Connection::PARAM_STR_ARRAY; } diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php index 3202dae97f5c2..3c1141c54860d 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php @@ -24,11 +24,9 @@ class CollectionToArrayTransformer implements DataTransformerInterface /** * Transforms a collection into an array. * - * @return mixed An array of entities - * * @throws TransformationFailedException */ - public function transform($collection) + public function transform(mixed $collection): mixed { if (null === $collection) { return []; @@ -51,10 +49,8 @@ public function transform($collection) * Transforms choice keys into entities. * * @param mixed $array An array of entities - * - * @return Collection A collection of entities */ - public function reverseTransform($array) + public function reverseTransform(mixed $array): Collection { if ('' === $array || null === $array) { $array = []; diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index c2897c6d9aba8..75d7562369cce 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -14,6 +14,7 @@ use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\Form\FormTypeGuesserInterface; class DoctrineOrmExtension extends AbstractExtension { @@ -24,14 +25,14 @@ public function __construct(ManagerRegistry $registry) $this->registry = $registry; } - protected function loadTypes() + protected function loadTypes(): array { return [ new EntityType($this->registry), ]; } - protected function loadTypeGuesser() + protected function loadTypeGuesser(): ?FormTypeGuesserInterface { return new DoctrineOrmTypeGuesser($this->registry); } diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index 944c305ab70a7..191a853ac5c93 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -11,7 +11,6 @@ namespace Symfony\Bridge\Doctrine\Form; -use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as LegacyMappingException; @@ -27,23 +26,17 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface { protected $registry; - private $cache = []; - - private static $useDeprecatedConstants; + private array $cache = []; public function __construct(ManagerRegistry $registry) { $this->registry = $registry; - - if (null === self::$useDeprecatedConstants) { - self::$useDeprecatedConstants = !class_exists(Types::class); - } } /** * {@inheritdoc} */ - public function guessType($class, $property) + public function guessType(string $class, string $property): ?TypeGuess { if (!$ret = $this->getMetadata($class)) { return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); @@ -59,44 +52,39 @@ public function guessType($class, $property) } switch ($metadata->getTypeOfField($property)) { - case self::$useDeprecatedConstants ? Type::TARRAY : Types::ARRAY: - // no break - case self::$useDeprecatedConstants ? Type::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: + case Types::ARRAY: + case Types::SIMPLE_ARRAY: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::BOOLEAN : Types::BOOLEAN: + case Types::BOOLEAN: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', [], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE: - // no break - case self::$useDeprecatedConstants ? Type::DATETIMETZ : Types::DATETIMETZ_MUTABLE: - // no break + case Types::DATETIME_MUTABLE: + case Types::DATETIMETZ_MUTABLE: case 'vardatetime': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', [], Guess::HIGH_CONFIDENCE); - case 'datetime_immutable': - case 'datetimetz_immutable': + case Types::DATETIME_IMMUTABLE: + case Types::DATETIMETZ_IMMUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case 'dateinterval': + case Types::DATEINTERVAL: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', [], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::DATE : Types::DATE_MUTABLE: + case Types::DATE_MUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::HIGH_CONFIDENCE); - case 'date_immutable': + case Types::DATE_IMMUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::TIME : Types::TIME_MUTABLE: + case Types::TIME_MUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', [], Guess::HIGH_CONFIDENCE); - case 'time_immutable': + case Types::TIME_IMMUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::DECIMAL : Types::DECIMAL: + case Types::DECIMAL: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', ['input' => 'string'], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::FLOAT : Types::FLOAT: + case Types::FLOAT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::INTEGER : Types::INTEGER: - // no break - case self::$useDeprecatedConstants ? Type::BIGINT : Types::BIGINT: - // no break - case self::$useDeprecatedConstants ? Type::SMALLINT : Types::SMALLINT: + case Types::INTEGER: + case Types::BIGINT: + case Types::SMALLINT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::STRING : Types::STRING: + case Types::STRING: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::MEDIUM_CONFIDENCE); - case self::$useDeprecatedConstants ? Type::TEXT : Types::TEXT: + case Types::TEXT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextareaType', [], Guess::MEDIUM_CONFIDENCE); default: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); @@ -106,7 +94,7 @@ public function guessType($class, $property) /** * {@inheritdoc} */ - public function guessRequired($class, $property) + public function guessRequired(string $class, string $property): ?ValueGuess { $classMetadatas = $this->getMetadata($class); @@ -119,7 +107,7 @@ public function guessRequired($class, $property) // Check whether the field exists and is nullable or not if (isset($classMetadata->fieldMappings[$property])) { - if (!$classMetadata->isNullable($property) && (self::$useDeprecatedConstants ? Type::BOOLEAN : Types::BOOLEAN) !== $classMetadata->getTypeOfField($property)) { + if (!$classMetadata->isNullable($property) && Types::BOOLEAN !== $classMetadata->getTypeOfField($property)) { return new ValueGuess(true, Guess::HIGH_CONFIDENCE); } @@ -146,7 +134,7 @@ public function guessRequired($class, $property) /** * {@inheritdoc} */ - public function guessMaxLength($class, $property) + public function guessMaxLength(string $class, string $property): ?ValueGuess { $ret = $this->getMetadata($class); if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) { @@ -156,7 +144,7 @@ public function guessMaxLength($class, $property) return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE); } - if (\in_array($ret[0]->getTypeOfField($property), self::$useDeprecatedConstants ? [Type::DECIMAL, Type::FLOAT] : [Types::DECIMAL, Types::FLOAT])) { + if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } } @@ -167,11 +155,11 @@ public function guessMaxLength($class, $property) /** * {@inheritdoc} */ - public function guessPattern($class, $property) + public function guessPattern(string $class, string $property): ?ValueGuess { $ret = $this->getMetadata($class); if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) { - if (\in_array($ret[0]->getTypeOfField($property), self::$useDeprecatedConstants ? [Type::DECIMAL, Type::FLOAT] : [Types::DECIMAL, Types::FLOAT])) { + if (\in_array($ret[0]->getTypeOfField($property), [Types::DECIMAL, Types::FLOAT])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } } @@ -179,7 +167,7 @@ public function guessPattern($class, $property) return null; } - protected function getMetadata($class) + protected function getMetadata(string $class) { // normalize class name $class = self::getRealClass(ltrim($class, '\\')); diff --git a/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php b/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php index 1ec496b781c5d..f16fc64fd3ff4 100644 --- a/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php +++ b/src/Symfony/Bridge/Doctrine/Form/EventListener/MergeDoctrineCollectionListener.php @@ -27,7 +27,7 @@ */ class MergeDoctrineCollectionListener implements EventSubscriberInterface { - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { // Higher priority than core MergeCollectionListener so that this one // is called before diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 3a77e74c560b0..f91d240451470 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -20,6 +20,7 @@ use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; @@ -38,24 +39,22 @@ abstract class DoctrineType extends AbstractType implements ResetInterface /** * @var IdReader[] */ - private $idReaders = []; + private array $idReaders = []; /** - * @var DoctrineChoiceLoader[] + * @var EntityLoaderInterface[] */ - private $choiceLoaders = []; + private array $entityLoaders = []; /** * Creates the label for a choice. * * For backwards compatibility, objects are cast to strings by default. * - * @param object $choice The object - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public static function createChoiceLabel($choice): string + public static function createChoiceLabel(object $choice): string { return (string) $choice; } @@ -67,17 +66,14 @@ public static function createChoiceLabel($choice): string * a single-column integer ID. In that case, the value of the field is * the ID of the object. That ID is also used as field name. * - * @param object $choice The object - * @param int|string $key The choice key - * @param string $value The choice value. Corresponds to the object's - * ID here. + * @param string $value The choice value. Corresponds to the object's ID here. * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public static function createChoiceName($choice, $key, $value): string + public static function createChoiceName(object $choice, int|string $key, string $value): string { - return str_replace('-', '_', (string) $value); + return str_replace('-', '_', $value); } /** @@ -88,13 +84,10 @@ public static function createChoiceName($choice, $key, $value): string * @param object $queryBuilder A query builder, type declaration is not present here as there * is no common base class for the different implementations * - * @return array|null Array with important QueryBuilder parts or null if - * they can't be determined - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array + public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array { return null; } @@ -119,44 +112,26 @@ public function configureOptions(OptionsResolver $resolver) $choiceLoader = function (Options $options) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { - $hash = null; - $qbParts = null; + // If there is no QueryBuilder we can safely cache + $vary = [$options['em'], $options['class']]; - // If there is no QueryBuilder we can safely cache DoctrineChoiceLoader, // also if concrete Type can return important QueryBuilder parts to generate - // hash key we go for it as well - if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) { - $hash = CachingFactoryDecorator::generateHash([ - $options['em'], - $options['class'], - $qbParts, - ]); - - if (isset($this->choiceLoaders[$hash])) { - return $this->choiceLoaders[$hash]; - } - } - - if (null !== $options['query_builder']) { - $entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']); - } else { - $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); - $entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']); + // hash key we go for it as well, otherwise fallback on the instance + if ($options['query_builder']) { + $vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder']; } - $doctrineChoiceLoader = new DoctrineChoiceLoader( + return ChoiceList::loader($this, new DoctrineChoiceLoader( $options['em'], $options['class'], $options['id_reader'], - $entityLoader, - false - ); - - if (null !== $hash) { - $this->choiceLoaders[$hash] = $doctrineChoiceLoader; - } - - return $doctrineChoiceLoader; + $this->getCachedEntityLoader( + $options['em'], + $options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'), + $options['class'], + $vary + ) + ), $vary); } return null; @@ -167,7 +142,7 @@ public function configureOptions(OptionsResolver $resolver) // field name. We can only use numeric IDs as names, as we cannot // guarantee that a non-numeric ID contains a valid form name if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) { - return [__CLASS__, 'createChoiceName']; + return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']); } // Otherwise, an incrementing integer is used as name automatically @@ -181,7 +156,7 @@ public function configureOptions(OptionsResolver $resolver) $choiceValue = function (Options $options) { // If the entity has a single-column ID, use that ID as value if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) { - return [$options['id_reader'], 'getIdValue']; + return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']); } // Otherwise, an incrementing integer is used as value automatically @@ -219,27 +194,13 @@ public function configureOptions(OptionsResolver $resolver) // Set the "id_reader" option via the normalizer. This option is not // supposed to be set by the user. $idReaderNormalizer = function (Options $options) { - $hash = CachingFactoryDecorator::generateHash([ - $options['em'], - $options['class'], - ]); - // The ID reader is a utility that is needed to read the object IDs // when generating the field values. The callback generating the // field values has no access to the object manager or the class // of the field, so we store that information in the reader. // The reader is cached so that two choice lists for the same class // (and hence with the same reader) can successfully be cached. - if (!isset($this->idReaders[$hash])) { - $classMetadata = $options['em']->getClassMetadata($options['class']); - $this->idReaders[$hash] = new IdReader($options['em'], $classMetadata); - } - - if ($this->idReaders[$hash]->isSingleId()) { - return $this->idReaders[$hash]; - } - - return null; + return $this->getCachedIdReader($options['em'], $options['class']); }; $resolver->setDefaults([ @@ -247,7 +208,7 @@ public function configureOptions(OptionsResolver $resolver) 'query_builder' => null, 'choices' => null, 'choice_loader' => $choiceLoader, - 'choice_label' => [__CLASS__, 'createChoiceLabel'], + 'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']), 'choice_name' => $choiceName, 'choice_value' => $choiceValue, 'id_reader' => null, // internal @@ -265,15 +226,10 @@ public function configureOptions(OptionsResolver $resolver) /** * Return the default loader object. - * - * @param mixed $queryBuilder - * @param string $class - * - * @return EntityLoaderInterface */ - abstract public function getLoader(ObjectManager $manager, $queryBuilder, $class); + abstract public function getLoader(ObjectManager $manager, object $queryBuilder, string $class): EntityLoaderInterface; - public function getParent() + public function getParent(): string { return ChoiceType::class; } @@ -281,8 +237,27 @@ public function getParent() public function reset() { $this->idReaders = []; - $this->choiceLoaders = []; + $this->entityLoaders = []; + } + + private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader + { + $hash = CachingFactoryDecorator::generateHash([$manager, $class]); + + if (isset($this->idReaders[$hash])) { + return $this->idReaders[$hash]; + } + + $idReader = new IdReader($manager, $manager->getClassMetadata($class)); + + // don't cache the instance for composite ids that cannot be optimized + return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null; } -} -interface_exists(ObjectManager::class); + private function getCachedEntityLoader(ObjectManager $manager, object $queryBuilder, string $class, array $vary): EntityLoaderInterface + { + $hash = CachingFactoryDecorator::generateHash($vary); + + return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 69c92c0b08389..6b73af766536f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -32,7 +32,7 @@ public function configureOptions(OptionsResolver $resolver) $queryBuilder = $queryBuilder($options['em']->getRepository($options['class'])); if (null !== $queryBuilder && !$queryBuilder instanceof QueryBuilder) { - throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + throw new UnexpectedTypeException($queryBuilder, QueryBuilder::class); } } @@ -40,21 +40,18 @@ public function configureOptions(OptionsResolver $resolver) }; $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); - $resolver->setAllowedTypes('query_builder', ['null', 'callable', 'Doctrine\ORM\QueryBuilder']); + $resolver->setAllowedTypes('query_builder', ['null', 'callable', QueryBuilder::class]); } /** * Return the default loader object. * * @param QueryBuilder $queryBuilder - * @param string $class - * - * @return ORMQueryBuilderLoader */ - public function getLoader(ObjectManager $manager, $queryBuilder, $class) + public function getLoader(ObjectManager $manager, object $queryBuilder, string $class): ORMQueryBuilderLoader { if (!$queryBuilder instanceof QueryBuilder) { - throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder))); } return new ORMQueryBuilderLoader($queryBuilder); @@ -63,7 +60,7 @@ public function getLoader(ObjectManager $manager, $queryBuilder, $class) /** * {@inheritdoc} */ - public function getBlockPrefix() + public function getBlockPrefix(): string { return 'entity'; } @@ -77,10 +74,10 @@ public function getBlockPrefix() * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array + public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array { if (!$queryBuilder instanceof QueryBuilder) { - throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder))); } return [ @@ -97,5 +94,3 @@ private function parameterToArray(Parameter $parameter): array return [$parameter->getName(), $parameter->getType(), $parameter->getValue()]; } } - -interface_exists(ObjectManager::class); diff --git a/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php new file mode 100644 index 0000000000000..74dc9ec250f2b --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/IdGenerator/UlidGenerator.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\IdGenerator; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Id\AbstractIdGenerator; +use Symfony\Component\Uid\Factory\UlidFactory; +use Symfony\Component\Uid\Ulid; + +final class UlidGenerator extends AbstractIdGenerator +{ + private $factory; + + public function __construct(UlidFactory $factory = null) + { + $this->factory = $factory; + } + + /** + * doctrine/orm < 2.11 BC layer. + */ + public function generate(EntityManager $em, $entity): Ulid + { + return $this->generateId($em, $entity); + } + + public function generateId(EntityManagerInterface $em, $entity): Ulid + { + if ($this->factory) { + return $this->factory->create(); + } + + return new Ulid(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php b/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php new file mode 100644 index 0000000000000..d72dc327c2fe8 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/IdGenerator/UuidGenerator.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\IdGenerator; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Id\AbstractIdGenerator; +use Symfony\Component\Uid\Factory\UuidFactory; +use Symfony\Component\Uid\Uuid; + +final class UuidGenerator extends AbstractIdGenerator +{ + private $protoFactory; + private $factory; + private ?string $entityGetter = null; + + public function __construct(UuidFactory $factory = null) + { + $this->protoFactory = $this->factory = $factory ?? new UuidFactory(); + } + + /** + * doctrine/orm < 2.11 BC layer. + */ + public function generate(EntityManager $em, $entity): Uuid + { + return $this->generateId($em, $entity); + } + + public function generateId(EntityManagerInterface $em, $entity): Uuid + { + if (null !== $this->entityGetter) { + if (\is_callable([$entity, $this->entityGetter])) { + return $this->factory->create($entity->{$this->entityGetter}()); + } + + return $this->factory->create($entity->{$this->entityGetter}); + } + + return $this->factory->create(); + } + + public function nameBased(string $entityGetter, Uuid|string $namespace = null): static + { + $clone = clone $this; + $clone->factory = $clone->protoFactory->nameBased($namespace); + $clone->entityGetter = $entityGetter; + + return $clone; + } + + public function randomBased(): static + { + $clone = clone $this; + $clone->factory = $clone->protoFactory->randomBased(); + $clone->entityGetter = null; + + return $clone; + } + + public function timeBased(Uuid|string $node = null): static + { + $clone = clone $this; + $clone->factory = $clone->protoFactory->timeBased($node); + $clone->entityGetter = null; + + return $clone; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php b/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php index f7d2ae00e5df9..f9bd6e0cbcf73 100644 --- a/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php +++ b/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php @@ -34,10 +34,8 @@ public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch /** * {@inheritdoc} - * - * @return void */ - public function startQuery($sql, array $params = null, array $types = null) + public function startQuery($sql, array $params = null, array $types = null): void { if (null !== $this->stopwatch) { $this->stopwatch->start('doctrine', 'doctrine'); @@ -50,10 +48,8 @@ public function startQuery($sql, array $params = null, array $types = null) /** * {@inheritdoc} - * - * @return void */ - public function stopQuery() + public function stopQuery(): void { if (null !== $this->stopwatch) { $this->stopwatch->stop('doctrine'); @@ -62,11 +58,8 @@ public function stopQuery() /** * Logs a message. - * - * @param string $message A message to log - * @param array $params The context */ - protected function log($message, array $params) + protected function log(string $message, array $params) { $this->logger->debug($message, $params); } diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index 0ed055fec64b7..7be0473590231 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -30,20 +30,16 @@ abstract class ManagerRegistry extends AbstractManagerRegistry /** * {@inheritdoc} - * - * @return object */ - protected function getService($name) + protected function getService($name): object { return $this->container->get($name); } /** * {@inheritdoc} - * - * @return void */ - protected function resetService($name) + protected function resetService($name): void { if (!$this->container->initialized($name)) { return; @@ -55,17 +51,13 @@ protected function resetService($name) } $manager->setProxyInitializer(\Closure::bind( function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { - if (isset($this->normalizedIds[$normalizedId = strtolower($name)])) { // BC with DI v3.4 - $name = $this->normalizedIds[$normalizedId]; - } if (isset($this->aliases[$name])) { $name = $this->aliases[$name]; } if (isset($this->fileMap[$name])) { $wrappedInstance = $this->load($this->fileMap[$name], false); } else { - $method = $this->methodMap[$name] ?? 'get'.strtr($name, $this->underscoreMap).'Service'; // BC with DI v3.4 - $wrappedInstance = $this->{$method}(false); + $wrappedInstance = $this->{$this->methodMap[$name]}(false); } $manager->setProxyInitializer(null); diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php index d702186a713ce..e06ba250b8e17 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php @@ -40,10 +40,12 @@ public function onWorkerMessageFailed() $this->clearEntityManagers(); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { - yield WorkerMessageHandledEvent::class => 'onWorkerMessageHandled'; - yield WorkerMessageFailedEvent::class => 'onWorkerMessageFailed'; + return [ + WorkerMessageHandledEvent::class => 'onWorkerMessageHandled', + WorkerMessageFailedEvent::class => 'onWorkerMessageFailed', + ]; } private function clearEntityManagers() diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineOpenTransactionLoggerMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineOpenTransactionLoggerMiddleware.php new file mode 100644 index 0000000000000..246f0090e58ef --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineOpenTransactionLoggerMiddleware.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Messenger; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Middleware\StackInterface; + +/** + * Middleware to log when transaction has been left open. + * + * @author Grégoire Pineau + */ +class DoctrineOpenTransactionLoggerMiddleware extends AbstractDoctrineMiddleware +{ + private $logger; + + public function __construct(ManagerRegistry $managerRegistry, string $entityManagerName = null, LoggerInterface $logger = null) + { + parent::__construct($managerRegistry, $entityManagerName); + + $this->logger = $logger ?? new NullLogger(); + } + + protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope + { + try { + return $stack->next()->handle($envelope, $stack); + } finally { + if ($entityManager->getConnection()->isTransactionActive()) { + $this->logger->error('A handler opened a transaction but did not close it.', [ + 'message' => $envelope->getMessage(), + ]); + } + } + } +} diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php index 30f12129c2719..de925284d09dc 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php @@ -11,8 +11,7 @@ namespace Symfony\Bridge\Doctrine\Messenger; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception as DBALException; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Middleware\StackInterface; @@ -40,7 +39,7 @@ private function pingConnection(EntityManagerInterface $entityManager) try { $connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL()); - } catch (DBALException|Exception $e) { + } catch (DBALException $e) { $connection->close(); $connection->connect(); } diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index 9add7946fbea5..9e294b2fdbd30 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -11,13 +11,13 @@ namespace Symfony\Bridge\Doctrine\PropertyInfo; -use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\Embedded; use Doctrine\ORM\Mapping\MappingException as OrmMappingException; -use Doctrine\Persistence\Mapping\ClassMetadataFactory; use Doctrine\Persistence\Mapping\MappingException; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; @@ -32,33 +32,16 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface { private $entityManager; - private $classMetadataFactory; - private static $useDeprecatedConstants; - - /** - * @param EntityManagerInterface $entityManager - */ - public function __construct($entityManager) + public function __construct(EntityManagerInterface $entityManager) { - if ($entityManager instanceof EntityManagerInterface) { - $this->entityManager = $entityManager; - } elseif ($entityManager instanceof ClassMetadataFactory) { - @trigger_error(sprintf('Injecting an instance of "%s" in "%s" is deprecated since Symfony 4.2, inject an instance of "%s" instead.', ClassMetadataFactory::class, __CLASS__, EntityManagerInterface::class), \E_USER_DEPRECATED); - $this->classMetadataFactory = $entityManager; - } else { - throw new \TypeError(sprintf('$entityManager must be an instance of "%s", "%s" given.', EntityManagerInterface::class, \is_object($entityManager) ? \get_class($entityManager) : \gettype($entityManager))); - } - - if (null === self::$useDeprecatedConstants) { - self::$useDeprecatedConstants = !class_exists(Types::class); - } + $this->entityManager = $entityManager; } /** * {@inheritdoc} */ - public function getProperties($class, array $context = []) + public function getProperties(string $class, array $context = []): ?array { if (null === $metadata = $this->getMetadata($class)) { return null; @@ -66,7 +49,7 @@ public function getProperties($class, array $context = []) $properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); - if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && $metadata->embeddedClasses) { + if ($metadata instanceof ClassMetadataInfo && class_exists(Embedded::class) && $metadata->embeddedClasses) { $properties = array_filter($properties, function ($property) { return !str_contains($property, '.'); }); @@ -80,7 +63,7 @@ public function getProperties($class, array $context = []) /** * {@inheritdoc} */ - public function getTypes($class, $property, array $context = []) + public function getTypes(string $class, string $property, array $context = []): ?array { if (null === $metadata = $this->getMetadata($class)) { return null; @@ -108,7 +91,7 @@ public function getTypes($class, $property, array $context = []) if (isset($associationMapping['indexBy'])) { /** @var ClassMetadataInfo $subMetadata */ - $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); + $subMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']); // Check if indexBy value is a property $fieldName = $associationMapping['indexBy']; @@ -121,7 +104,7 @@ public function getTypes($class, $property, array $context = []) /** @var ClassMetadataInfo $subMetadata */ $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName); - $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); + $subMetadata = $this->entityManager->getClassMetadata($associationMapping['targetEntity']); //Not a property, maybe a column name? if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) { @@ -140,14 +123,14 @@ public function getTypes($class, $property, array $context = []) return [new Type( Type::BUILTIN_TYPE_OBJECT, false, - 'Doctrine\Common\Collections\Collection', + Collection::class, true, new Type($collectionKeyType), new Type(Type::BUILTIN_TYPE_OBJECT, false, $class) )]; } - if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && isset($metadata->embeddedClasses[$property])) { + if ($metadata instanceof ClassMetadataInfo && class_exists(Embedded::class) && isset($metadata->embeddedClasses[$property])) { return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $metadata->embeddedClasses[$property]['class'])]; } @@ -166,35 +149,31 @@ public function getTypes($class, $property, array $context = []) switch ($builtinType) { case Type::BUILTIN_TYPE_OBJECT: switch ($typeOfField) { - case self::$useDeprecatedConstants ? DBALType::DATE : Types::DATE_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIME : Types::DATETIME_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIMETZ : Types::DATETIMETZ_MUTABLE: - // no break + case Types::DATE_MUTABLE: + case Types::DATETIME_MUTABLE: + case Types::DATETIMETZ_MUTABLE: case 'vardatetime': - case self::$useDeprecatedConstants ? DBALType::TIME : Types::TIME_MUTABLE: + case Types::TIME_MUTABLE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; - case 'date_immutable': - case 'datetime_immutable': - case 'datetimetz_immutable': - case 'time_immutable': + case Types::DATE_IMMUTABLE: + case Types::DATETIME_IMMUTABLE: + case Types::DATETIMETZ_IMMUTABLE: + case Types::TIME_IMMUTABLE: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; - case 'dateinterval': + case Types::DATEINTERVAL: return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; } break; case Type::BUILTIN_TYPE_ARRAY: switch ($typeOfField) { - case self::$useDeprecatedConstants ? DBALType::TARRAY : Types::ARRAY: - // no break + case Types::ARRAY: case 'json_array': return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; - case self::$useDeprecatedConstants ? DBALType::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: + case Types::SIMPLE_ARRAY: return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]; } } @@ -208,7 +187,7 @@ public function getTypes($class, $property, array $context = []) /** * {@inheritdoc} */ - public function isReadable($class, $property, array $context = []) + public function isReadable(string $class, string $property, array $context = []): ?bool { return null; } @@ -216,7 +195,7 @@ public function isReadable($class, $property, array $context = []) /** * {@inheritdoc} */ - public function isWritable($class, $property, array $context = []) + public function isWritable(string $class, string $property, array $context = []): ?bool { if ( null === ($metadata = $this->getMetadata($class)) @@ -232,7 +211,7 @@ public function isWritable($class, $property, array $context = []) private function getMetadata(string $class): ?ClassMetadata { try { - return $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class); + return $this->entityManager->getClassMetadata($class); } catch (MappingException|OrmMappingException $exception) { return null; } @@ -269,55 +248,42 @@ private function isAssociationNullable(array $associationMapping): bool private function getPhpType(string $doctrineType): ?string { switch ($doctrineType) { - case self::$useDeprecatedConstants ? DBALType::SMALLINT : Types::SMALLINT: - // no break - case self::$useDeprecatedConstants ? DBALType::INTEGER : Types::INTEGER: + case Types::SMALLINT: + case Types::INTEGER: return Type::BUILTIN_TYPE_INT; - case self::$useDeprecatedConstants ? DBALType::FLOAT : Types::FLOAT: + case Types::FLOAT: return Type::BUILTIN_TYPE_FLOAT; - case self::$useDeprecatedConstants ? DBALType::BIGINT : Types::BIGINT: - // no break - case self::$useDeprecatedConstants ? DBALType::STRING : Types::STRING: - // no break - case self::$useDeprecatedConstants ? DBALType::TEXT : Types::TEXT: - // no break - case self::$useDeprecatedConstants ? DBALType::GUID : Types::GUID: - // no break - case self::$useDeprecatedConstants ? DBALType::DECIMAL : Types::DECIMAL: + case Types::BIGINT: + case Types::STRING: + case Types::TEXT: + case Types::GUID: + case Types::DECIMAL: return Type::BUILTIN_TYPE_STRING; - case self::$useDeprecatedConstants ? DBALType::BOOLEAN : Types::BOOLEAN: + case Types::BOOLEAN: return Type::BUILTIN_TYPE_BOOL; - case self::$useDeprecatedConstants ? DBALType::BLOB : Types::BLOB: - // no break - case 'binary': + case Types::BLOB: + case Types::BINARY: return Type::BUILTIN_TYPE_RESOURCE; - case self::$useDeprecatedConstants ? DBALType::OBJECT : Types::OBJECT: - // no break - case self::$useDeprecatedConstants ? DBALType::DATE : Types::DATE_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIME : Types::DATETIME_MUTABLE: - // no break - case self::$useDeprecatedConstants ? DBALType::DATETIMETZ : Types::DATETIMETZ_MUTABLE: - // no break + case Types::OBJECT: + case Types::DATE_MUTABLE: + case Types::DATETIME_MUTABLE: + case Types::DATETIMETZ_MUTABLE: case 'vardatetime': - case self::$useDeprecatedConstants ? DBALType::TIME : Types::TIME_MUTABLE: - // no break - case 'date_immutable': - case 'datetime_immutable': - case 'datetimetz_immutable': - case 'time_immutable': - case 'dateinterval': + case Types::TIME_MUTABLE: + case Types::DATE_IMMUTABLE: + case Types::DATETIME_IMMUTABLE: + case Types::DATETIMETZ_IMMUTABLE: + case Types::TIME_IMMUTABLE: + case Types::DATEINTERVAL: return Type::BUILTIN_TYPE_OBJECT; - case self::$useDeprecatedConstants ? DBALType::TARRAY : Types::ARRAY: - // no break - case self::$useDeprecatedConstants ? DBALType::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: - // no break + case Types::ARRAY: + case Types::SIMPLE_ARRAY: case 'json_array': return Type::BUILTIN_TYPE_ARRAY; } diff --git a/src/Symfony/Bridge/Doctrine/RegistryInterface.php b/src/Symfony/Bridge/Doctrine/RegistryInterface.php deleted file mode 100644 index e62b3ba49ca23..0000000000000 --- a/src/Symfony/Bridge/Doctrine/RegistryInterface.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine; - -use Doctrine\ORM\EntityManager; -use Doctrine\Persistence\ManagerRegistry; - -/** - * References Doctrine connections and entity managers. - * - * @deprecated since Symfony 4.4, use Doctrine\Persistence\ManagerRegistry instead - * - * @author Fabien Potencier - */ -interface RegistryInterface extends ManagerRegistry -{ - /** - * Gets the default entity manager name. - * - * @return string The default entity manager name - */ - public function getDefaultEntityManagerName(); - - /** - * Gets a named entity manager. - * - * @param string $name The entity manager name (null for the default one) - * - * @return EntityManager - */ - public function getEntityManager($name = null); - - /** - * Gets an array of all registered entity managers. - * - * @return array An array of EntityManager instances - */ - public function getEntityManagers(); - - /** - * Resets a named entity manager. - * - * This method is useful when an entity manager has been closed - * because of a rollbacked transaction AND when you think that - * it makes sense to get a new one to replace the closed one. - * - * Be warned that you will get a brand new entity manager as - * the existing one is not usable anymore. This means that any - * other object with a dependency on this entity manager will - * hold an obsolete reference. You can inject the registry instead - * to avoid this problem. - * - * @param string $name The entity manager name (null for the default one) - * - * @return EntityManager - */ - public function resetEntityManager($name = null); - - /** - * Resolves a registered namespace alias to the full namespace. - * - * This method looks for the alias in all registered entity managers. - * - * @param string $alias The alias - * - * @return string The full namespace - * - * @see Configuration::getEntityNamespace - */ - public function getEntityNamespace($alias); - - /** - * Gets all connection names. - * - * @return array An array of connection names - */ - public function getEntityManagerNames(); - - /** - * Gets the entity manager associated with a given class. - * - * @param string $class A Doctrine Entity class name - * - * @return EntityManager|null - */ - public function getEntityManagerForClass($class); -} diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriber.php new file mode 100644 index 0000000000000..bf9b793175f3f --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriber.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\SchemaListener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Doctrine\ORM\Tools\ToolEvents; +use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; + +/** + * Automatically adds the cache table needed for the DoctrineDbalAdapter of + * the Cache component. + * + * @author Ryan Weaver + */ +final class DoctrineDbalCacheAdapterSchemaSubscriber implements EventSubscriber +{ + private $dbalAdapters; + + /** + * @param iterable $dbalAdapters + */ + public function __construct(iterable $dbalAdapters) + { + $this->dbalAdapters = $dbalAdapters; + } + + public function postGenerateSchema(GenerateSchemaEventArgs $event): void + { + $dbalConnection = $event->getEntityManager()->getConnection(); + foreach ($this->dbalAdapters as $dbalAdapter) { + $dbalAdapter->configureSchema($event->getSchema(), $dbalConnection); + } + } + + public function getSubscribedEvents(): array + { + if (!class_exists(ToolEvents::class)) { + return []; + } + + return [ + ToolEvents::postGenerateSchema, + ]; + } +} diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php new file mode 100644 index 0000000000000..3cf100615a51c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\SchemaListener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\DBAL\Event\SchemaCreateTableEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Doctrine\ORM\Tools\ToolEvents; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * Automatically adds any required database tables to the Doctrine Schema. + * + * @author Ryan Weaver + */ +final class MessengerTransportDoctrineSchemaSubscriber implements EventSubscriber +{ + private const PROCESSING_TABLE_FLAG = self::class.':processing'; + + private iterable $transports; + + /** + * @param iterable $transports + */ + public function __construct(iterable $transports) + { + $this->transports = $transports; + } + + public function postGenerateSchema(GenerateSchemaEventArgs $event): void + { + $dbalConnection = $event->getEntityManager()->getConnection(); + foreach ($this->transports as $transport) { + if (!$transport instanceof DoctrineTransport) { + continue; + } + + $transport->configureSchema($event->getSchema(), $dbalConnection); + } + } + + public function onSchemaCreateTable(SchemaCreateTableEventArgs $event): void + { + $table = $event->getTable(); + + // if this method triggers a nested create table below, allow Doctrine to work like normal + if ($table->hasOption(self::PROCESSING_TABLE_FLAG)) { + return; + } + + foreach ($this->transports as $transport) { + if (!$transport instanceof DoctrineTransport) { + continue; + } + + if (!$extraSql = $transport->getExtraSetupSqlForTable($table)) { + continue; + } + + // avoid this same listener from creating a loop on this table + $table->addOption(self::PROCESSING_TABLE_FLAG, true); + $createTableSql = $event->getPlatform()->getCreateTableSQL($table); + + /* + * Add all the SQL needed to create the table and tell Doctrine + * to "preventDefault" so that only our SQL is used. This is + * the only way to inject some extra SQL. + */ + $event->addSql($createTableSql); + foreach ($extraSql as $sql) { + $event->addSql($sql); + } + $event->preventDefault(); + + return; + } + } + + public function getSubscribedEvents(): array + { + $subscribedEvents = []; + + if (class_exists(ToolEvents::class)) { + $subscribedEvents[] = ToolEvents::postGenerateSchema; + } + + if (class_exists(Events::class)) { + $subscribedEvents[] = Events::onSchemaCreateTable; + } + + return $subscribedEvents; + } +} diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php new file mode 100644 index 0000000000000..2eba94ff23c06 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/RememberMeTokenProviderDoctrineSchemaSubscriber.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\SchemaListener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Doctrine\ORM\Tools\ToolEvents; +use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider; +use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler; +use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; + +/** + * Automatically adds the rememberme table needed for the {@see DoctrineTokenProvider}. + * + * @author Wouter de Jong + */ +final class RememberMeTokenProviderDoctrineSchemaSubscriber implements EventSubscriber +{ + private iterable $rememberMeHandlers; + + /** + * @param iterable $rememberMeHandlers + */ + public function __construct(iterable $rememberMeHandlers) + { + $this->rememberMeHandlers = $rememberMeHandlers; + } + + public function postGenerateSchema(GenerateSchemaEventArgs $event): void + { + $dbalConnection = $event->getEntityManager()->getConnection(); + + foreach ($this->rememberMeHandlers as $rememberMeHandler) { + if ( + $rememberMeHandler instanceof PersistentRememberMeHandler + && ($tokenProvider = $rememberMeHandler->getTokenProvider()) instanceof DoctrineTokenProvider + ) { + $tokenProvider->configureSchema($event->getSchema(), $dbalConnection); + } + } + } + + public function getSubscribedEvents(): array + { + if (!class_exists(ToolEvents::class)) { + return []; + } + + return [ + ToolEvents::postGenerateSchema, + ]; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index 8f8256f6cb99b..780999f4385ce 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -15,15 +15,16 @@ use Doctrine\DBAL\Driver\Result as DriverResult; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Result; -use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Types; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface; use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface; +use Symfony\Component\Security\Core\Authentication\RememberMe\TokenVerifierInterface; use Symfony\Component\Security\Core\Exception\TokenNotFoundException; /** - * This class provides storage for the tokens that is set in "remember me" + * This class provides storage for the tokens that is set in "remember-me" * cookies. This way no password secrets will be stored in the cookies on * the client machine, and thus the security is improved. * @@ -40,29 +41,22 @@ * `username` varchar(200) NOT NULL * ); */ -class DoctrineTokenProvider implements TokenProviderInterface +class DoctrineTokenProvider implements TokenProviderInterface, TokenVerifierInterface { private $conn; - private static $useDeprecatedConstants; - public function __construct(Connection $conn) { $this->conn = $conn; - - if (null === self::$useDeprecatedConstants) { - self::$useDeprecatedConstants = !class_exists(Types::class); - } } /** * {@inheritdoc} */ - public function loadTokenBySeries($series) + public function loadTokenBySeries(string $series): PersistentTokenInterface { // the alias for lastUsed works around case insensitivity in PostgreSQL - $sql = 'SELECT class, username, value, lastUsed AS last_used' - .' FROM rememberme_token WHERE series=:series'; + $sql = 'SELECT class, username, value, lastUsed AS last_used FROM rememberme_token WHERE series=:series'; $paramValues = ['series' => $series]; $paramTypes = ['series' => ParameterType::STRING]; $stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes); @@ -78,7 +72,7 @@ public function loadTokenBySeries($series) /** * {@inheritdoc} */ - public function deleteTokenBySeries($series) + public function deleteTokenBySeries(string $series) { $sql = 'DELETE FROM rememberme_token WHERE series=:series'; $paramValues = ['series' => $series]; @@ -93,10 +87,9 @@ public function deleteTokenBySeries($series) /** * {@inheritdoc} */ - public function updateToken($series, $tokenValue, \DateTime $lastUsed) + public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed) { - $sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed' - .' WHERE series=:series'; + $sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed WHERE series=:series'; $paramValues = [ 'value' => $tokenValue, 'lastUsed' => $lastUsed, @@ -104,7 +97,7 @@ public function updateToken($series, $tokenValue, \DateTime $lastUsed) ]; $paramTypes = [ 'value' => ParameterType::STRING, - 'lastUsed' => self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE, + 'lastUsed' => Types::DATETIME_MUTABLE, 'series' => ParameterType::STRING, ]; if (method_exists($this->conn, 'executeStatement')) { @@ -122,12 +115,10 @@ public function updateToken($series, $tokenValue, \DateTime $lastUsed) */ public function createNewToken(PersistentTokenInterface $token) { - $sql = 'INSERT INTO rememberme_token' - .' (class, username, series, value, lastUsed)' - .' VALUES (:class, :username, :series, :value, :lastUsed)'; + $sql = 'INSERT INTO rememberme_token (class, username, series, value, lastUsed) VALUES (:class, :username, :series, :value, :lastUsed)'; $paramValues = [ 'class' => $token->getClass(), - 'username' => $token->getUsername(), + 'username' => $token->getUserIdentifier(), 'series' => $token->getSeries(), 'value' => $token->getTokenValue(), 'lastUsed' => $token->getLastUsed(), @@ -137,7 +128,7 @@ public function createNewToken(PersistentTokenInterface $token) 'username' => ParameterType::STRING, 'series' => ParameterType::STRING, 'value' => ParameterType::STRING, - 'lastUsed' => self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE, + 'lastUsed' => Types::DATETIME_MUTABLE, ]; if (method_exists($this->conn, 'executeStatement')) { $this->conn->executeStatement($sql, $paramValues, $paramTypes); @@ -145,4 +136,99 @@ public function createNewToken(PersistentTokenInterface $token) $this->conn->executeUpdate($sql, $paramValues, $paramTypes); } } + + /** + * {@inheritdoc} + */ + public function verifyToken(PersistentTokenInterface $token, string $tokenValue): bool + { + // Check if the token value matches the current persisted token + if (hash_equals($token->getTokenValue(), $tokenValue)) { + return true; + } + + // Generate an alternative series id here by changing the suffix == to _ + // this is needed to be able to store an older token value in the database + // which has a PRIMARY(series), and it works as long as series ids are + // generated using base64_encode(random_bytes(64)) which always outputs + // a == suffix, but if it should not work for some reason we abort + // for safety + $tmpSeries = preg_replace('{=+$}', '_', $token->getSeries()); + if ($tmpSeries === $token->getSeries()) { + return false; + } + + // Check if the previous token is present. If the given $tokenValue + // matches the previous token (and it is outdated by at most 60seconds) + // we also accept it as a valid value. + try { + $tmpToken = $this->loadTokenBySeries($tmpSeries); + } catch (TokenNotFoundException $e) { + return false; + } + + if ($tmpToken->getLastUsed()->getTimestamp() + 60 < time()) { + return false; + } + + return hash_equals($tmpToken->getTokenValue(), $tokenValue); + } + + /** + * {@inheritdoc} + */ + public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void + { + if (!$token instanceof PersistentToken) { + return; + } + + // Persist a copy of the previous token for authentication + // in verifyToken should the old token still be sent by the browser + // in a request concurrent to the one that did this token update + $tmpSeries = preg_replace('{=+$}', '_', $token->getSeries()); + // if we cannot generate a unique series it is not worth trying further + if ($tmpSeries === $token->getSeries()) { + return; + } + + $this->conn->beginTransaction(); + try { + $this->deleteTokenBySeries($tmpSeries); + $this->createNewToken(new PersistentToken($token->getClass(), $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed)); + + $this->conn->commit(); + } catch (\Exception $e) { + $this->conn->rollBack(); + throw $e; + } + } + + /** + * Adds the Table to the Schema if "remember me" uses this Connection. + */ + public function configureSchema(Schema $schema, Connection $forConnection): void + { + // only update the schema for this connection + if ($forConnection !== $this->conn) { + return; + } + + if ($schema->hasTable('rememberme_token')) { + return; + } + + $this->addTableToSchema($schema); + } + + private function addTableToSchema(Schema $schema): void + { + $table = $schema->createTable('rememberme_token'); + $table->addColumn('series', Types::STRING, ['length' => 88]); + $table->addColumn('value', Types::STRING, ['length' => 88]); + $table->addColumn('lastUsed', Types::DATETIME_MUTABLE); + $table->addColumn('class', Types::STRING, ['length' => 100]); + $table->addColumn('username', Types::STRING, ['length' => 200]); + $table->setPrimaryKey(['series']); + } } diff --git a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php index 213af6f36a235..b5ae03d549dee 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php @@ -16,7 +16,8 @@ use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\ObjectRepository; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -32,10 +33,10 @@ class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface { private $registry; - private $managerName; - private $classOrAlias; - private $class; - private $property; + private ?string $managerName; + private string $classOrAlias; + private string $class; + private ?string $property; public function __construct(ManagerRegistry $registry, string $classOrAlias, string $property = null, string $managerName = null) { @@ -45,25 +46,22 @@ public function __construct(ManagerRegistry $registry, string $classOrAlias, str $this->property = $property; } - /** - * {@inheritdoc} - */ - public function loadUserByUsername($username) + public function loadUserByIdentifier(string $identifier): UserInterface { $repository = $this->getRepository(); if (null !== $this->property) { - $user = $repository->findOneBy([$this->property => $username]); + $user = $repository->findOneBy([$this->property => $identifier]); } else { if (!$repository instanceof UserLoaderInterface) { - throw new \InvalidArgumentException(sprintf('You must either make the "%s" entity Doctrine Repository ("%s") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->classOrAlias, \get_class($repository))); + throw new \InvalidArgumentException(sprintf('You must either make the "%s" entity Doctrine Repository ("%s") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->classOrAlias, get_debug_type($repository))); } - $user = $repository->loadUserByUsername($username); + $user = $repository->loadUserByIdentifier($identifier); } if (null === $user) { - $e = new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); - $e->setUsername($username); + $e = new UserNotFoundException(sprintf('User "%s" not found.', $identifier)); + $e->setUserIdentifier($identifier); throw $e; } @@ -74,11 +72,11 @@ public function loadUserByUsername($username) /** * {@inheritdoc} */ - public function refreshUser(UserInterface $user) + public function refreshUser(UserInterface $user): UserInterface { $class = $this->getClass(); if (!$user instanceof $class) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } $repository = $this->getRepository(); @@ -95,8 +93,8 @@ public function refreshUser(UserInterface $user) $refreshedUser = $repository->find($id); if (null === $refreshedUser) { - $e = new UsernameNotFoundException('User with id '.json_encode($id).' not found.'); - $e->setUsername(json_encode($id)); + $e = new UserNotFoundException('User with id '.json_encode($id).' not found.'); + $e->setUserIdentifier(json_encode($id)); throw $e; } @@ -108,24 +106,26 @@ public function refreshUser(UserInterface $user) /** * {@inheritdoc} */ - public function supportsClass($class) + public function supportsClass(string $class): bool { return $class === $this->getClass() || is_subclass_of($class, $this->getClass()); } /** * {@inheritdoc} + * + * @final */ - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { $class = $this->getClass(); if (!$user instanceof $class) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } $repository = $this->getRepository(); if ($repository instanceof PasswordUpgraderInterface) { - $repository->upgradePassword($user, $newEncodedPassword); + $repository->upgradePassword($user, $newHashedPassword); } } @@ -141,7 +141,7 @@ private function getRepository(): ObjectRepository private function getClass(): string { - if (null === $this->class) { + if (!isset($this->class)) { $class = $this->classOrAlias; if (str_contains($class, ':')) { @@ -159,6 +159,3 @@ private function getClassMetadata(): ClassMetadata return $this->getObjectManager()->getClassMetadata($this->classOrAlias); } } - -interface_exists(ObjectManager::class); -interface_exists(ObjectRepository::class); diff --git a/src/Symfony/Bridge/Doctrine/Security/User/UserLoaderInterface.php b/src/Symfony/Bridge/Doctrine/Security/User/UserLoaderInterface.php index 452939fa7934a..e22e0bff05ead 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/UserLoaderInterface.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/UserLoaderInterface.php @@ -27,13 +27,9 @@ interface UserLoaderInterface { /** - * Loads the user for the given username. + * Loads the user for the given user identifier (e.g. username or email). * * This method must return null if the user is not found. - * - * @param string $username The username - * - * @return UserInterface|null */ - public function loadUserByUsername($username); + public function loadUserByIdentifier(string $identifier): ?UserInterface; } diff --git a/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php b/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php deleted file mode 100644 index 6197c6ae5169c..0000000000000 --- a/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Test; - -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Repository\RepositoryFactory; -use Doctrine\Persistence\ObjectRepository; - -/** - * @author Andreas Braun - */ -final class TestRepositoryFactory implements RepositoryFactory -{ - /** - * @var ObjectRepository[] - */ - private $repositoryList = []; - - /** - * {@inheritdoc} - * - * @return ObjectRepository - */ - public function getRepository(EntityManagerInterface $entityManager, $entityName) - { - $repositoryHash = $this->getRepositoryHash($entityManager, $entityName); - - if (isset($this->repositoryList[$repositoryHash])) { - return $this->repositoryList[$repositoryHash]; - } - - return $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName); - } - - public function setRepository(EntityManagerInterface $entityManager, string $entityName, ObjectRepository $repository) - { - $repositoryHash = $this->getRepositoryHash($entityManager, $entityName); - - $this->repositoryList[$repositoryHash] = $repository; - } - - private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository - { - /* @var $metadata ClassMetadata */ - $metadata = $entityManager->getClassMetadata($entityName); - $repositoryClassName = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName(); - - return new $repositoryClassName($entityManager, $metadata); - } - - private function getRepositoryHash(EntityManagerInterface $entityManager, string $entityName): string - { - return $entityManager->getClassMetadata($entityName)->getName().spl_object_hash($entityManager); - } -} diff --git a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php index e9d260ec26608..1631fa8ae37e7 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php @@ -27,10 +27,24 @@ protected function setUp(): void $this->evm = new ContainerAwareEventManager($this->container); } + public function testDispatchEventRespectOrder() + { + $this->evm = new ContainerAwareEventManager($this->container, ['sub1', [['foo'], 'list1'], 'sub2']); + + $this->container->set('list1', $listener1 = new MyListener()); + $this->container->set('sub1', $subscriber1 = new MySubscriber(['foo'])); + $this->container->set('sub2', $subscriber2 = new MySubscriber(['foo'])); + + $this->assertSame([$subscriber1, $listener1, $subscriber2], array_values($this->evm->getListeners('foo'))); + } + public function testDispatchEvent() { $this->evm = new ContainerAwareEventManager($this->container, ['lazy4']); + $this->container->set('lazy4', $subscriber1 = new MySubscriber(['foo'])); + $this->assertSame(0, $subscriber1->calledSubscribedEventsCount); + $this->container->set('lazy1', $listener1 = new MyListener()); $this->evm->addEventListener('foo', 'lazy1'); $this->evm->addEventListener('foo', $listener2 = new MyListener()); @@ -40,10 +54,8 @@ public function testDispatchEvent() $this->container->set('lazy3', $listener5 = new MyListener()); $this->evm->addEventListener('foo', $listener5 = new MyListener()); $this->evm->addEventListener('bar', $listener5); - $this->container->set('lazy4', $subscriber1 = new MySubscriber(['foo'])); $this->evm->addEventSubscriber($subscriber2 = new MySubscriber(['bar'])); - $this->assertSame(0, $subscriber1->calledSubscribedEventsCount); $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); $this->evm->dispatchEvent('foo'); @@ -72,8 +84,13 @@ public function testAddEventListenerAndSubscriberAfterDispatchEvent() { $this->evm = new ContainerAwareEventManager($this->container, ['lazy7']); + $this->container->set('lazy7', $subscriber1 = new MySubscriber(['foo'])); + $this->assertSame(0, $subscriber1->calledSubscribedEventsCount); + $this->container->set('lazy1', $listener1 = new MyListener()); $this->evm->addEventListener('foo', 'lazy1'); + $this->assertSame(1, $subscriber1->calledSubscribedEventsCount); + $this->evm->addEventListener('foo', $listener2 = new MyListener()); $this->container->set('lazy2', $listener3 = new MyListener()); $this->evm->addEventListener('bar', 'lazy2'); @@ -81,10 +98,8 @@ public function testAddEventListenerAndSubscriberAfterDispatchEvent() $this->container->set('lazy3', $listener5 = new MyListener()); $this->evm->addEventListener('foo', $listener5 = new MyListener()); $this->evm->addEventListener('bar', $listener5); - $this->container->set('lazy7', $subscriber1 = new MySubscriber(['foo'])); $this->evm->addEventSubscriber($subscriber2 = new MySubscriber(['bar'])); - $this->assertSame(0, $subscriber1->calledSubscribedEventsCount); $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); $this->evm->dispatchEvent('foo'); @@ -199,7 +214,7 @@ class MyListener public $calledByInvokeCount = 0; public $calledByEventNameCount = 0; - public function __invoke(): void + public function __invoke() { ++$this->calledByInvokeCount; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php index 28b983324e55d..e6fd198920517 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php @@ -85,18 +85,18 @@ public function testProcessEventListenersWithPriorities() $this->process($container); $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); - $methodCalls = $eventManagerDef->getMethodCalls(); $this->assertEquals( [ - ['addEventListener', [['foo_bar'], 'c']], - ['addEventListener', [['foo_bar'], 'a']], - ['addEventListener', [['bar'], 'a']], - ['addEventListener', [['foo'], 'b']], - ['addEventListener', [['foo'], 'a']], + [['foo_bar'], 'c'], + [['foo_bar'], 'a'], + [['bar'], 'a'], + [['foo'], 'b'], + [['foo'], 'a'], ], - $methodCalls + $eventManagerDef->getArgument(1) ); + $this->assertEquals([], $eventManagerDef->getMethodCalls()); $serviceLocatorDef = $container->getDefinition((string) $eventManagerDef->getArgument(0)); $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); @@ -114,6 +114,8 @@ public function testProcessEventListenersWithMultipleConnections() { $container = $this->createBuilder(true); + $container->setParameter('connection_param', 'second'); + $container ->register('a', 'stdClass') ->addTag('doctrine.event_listener', [ @@ -137,6 +139,14 @@ public function testProcessEventListenersWithMultipleConnections() ]) ; + $container + ->register('d', 'stdClass') + ->addTag('doctrine.event_listener', [ + 'event' => 'onFlush', + 'connection' => '%connection_param%', + ]) + ; + $this->process($container); $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); @@ -144,11 +154,12 @@ public function testProcessEventListenersWithMultipleConnections() // first connection $this->assertEquals( [ - ['addEventListener', [['onFlush'], 'a']], - ['addEventListener', [['onFlush'], 'b']], + [['onFlush'], 'a'], + [['onFlush'], 'b'], ], - $eventManagerDef->getMethodCalls() + $eventManagerDef->getArgument(1) ); + $this->assertEquals([], $eventManagerDef->getMethodCalls()); $serviceLocatorDef = $container->getDefinition((string) $eventManagerDef->getArgument(0)); $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); @@ -164,11 +175,13 @@ public function testProcessEventListenersWithMultipleConnections() $secondEventManagerDef = $container->getDefinition('doctrine.dbal.second_connection.event_manager'); $this->assertEquals( [ - ['addEventListener', [['onFlush'], 'a']], - ['addEventListener', [['onFlush'], 'c']], + [['onFlush'], 'a'], + [['onFlush'], 'c'], + [['onFlush'], 'd'], ], - $secondEventManagerDef->getMethodCalls() + $secondEventManagerDef->getArgument(1) ); + $this->assertEquals([], $secondEventManagerDef->getMethodCalls()); $serviceLocatorDef = $container->getDefinition((string) $secondEventManagerDef->getArgument(0)); $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); @@ -176,6 +189,7 @@ public function testProcessEventListenersWithMultipleConnections() [ 'a' => new ServiceClosureArgument(new Reference('a')), 'c' => new ServiceClosureArgument(new Reference('c')), + 'd' => new ServiceClosureArgument(new Reference('d')), ], $serviceLocatorDef->getArgument(0) ); @@ -185,6 +199,8 @@ public function testProcessEventSubscribersWithMultipleConnections() { $container = $this->createBuilder(true); + $container->setParameter('connection_param', 'second'); + $container ->register('a', 'stdClass') ->addTag('doctrine.event_subscriber', [ @@ -208,6 +224,14 @@ public function testProcessEventSubscribersWithMultipleConnections() ]) ; + $container + ->register('d', 'stdClass') + ->addTag('doctrine.event_subscriber', [ + 'event' => 'onFlush', + 'connection' => '%connection_param%', + ]) + ; + $this->process($container); $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); @@ -238,6 +262,7 @@ public function testProcessEventSubscribersWithMultipleConnections() [ 'a', 'c', + 'd', ], $eventManagerDef->getArgument(1) ); @@ -248,6 +273,7 @@ public function testProcessEventSubscribersWithMultipleConnections() [ 'a' => new ServiceClosureArgument(new Reference('a')), 'c' => new ServiceClosureArgument(new Reference('c')), + 'd' => new ServiceClosureArgument(new Reference('d')), ], $serviceLocatorDef->getArgument(0) ); @@ -315,6 +341,104 @@ public function testProcessEventSubscribersWithPriorities() ); } + public function testProcessEventSubscribersAndListenersWithPriorities() + { + $container = $this->createBuilder(); + + $container + ->register('a', 'stdClass') + ->addTag('doctrine.event_subscriber') + ; + $container + ->register('b', 'stdClass') + ->addTag('doctrine.event_subscriber', [ + 'priority' => 5, + ]) + ; + $container + ->register('c', 'stdClass') + ->addTag('doctrine.event_subscriber', [ + 'priority' => 10, + ]) + ; + $container + ->register('d', 'stdClass') + ->addTag('doctrine.event_subscriber', [ + 'priority' => 10, + ]) + ; + $container + ->register('e', 'stdClass') + ->addTag('doctrine.event_subscriber', [ + 'priority' => 10, + ]) + ; + $container + ->register('f', 'stdClass') + ->setPublic(false) + ->addTag('doctrine.event_listener', [ + 'event' => 'bar', + ]) + ->addTag('doctrine.event_listener', [ + 'event' => 'foo', + 'priority' => -5, + ]) + ->addTag('doctrine.event_listener', [ + 'event' => 'foo_bar', + 'priority' => 3, + ]) + ; + $container + ->register('g', 'stdClass') + ->addTag('doctrine.event_listener', [ + 'event' => 'foo', + ]) + ; + $container + ->register('h', 'stdClass') + ->addTag('doctrine.event_listener', [ + 'event' => 'foo_bar', + 'priority' => 4, + ]) + ; + + $this->process($container); + + $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); + + $this->assertEquals( + [ + 'c', + 'd', + 'e', + 'b', + [['foo_bar'], 'h'], + [['foo_bar'], 'f'], + 'a', + [['bar'], 'f'], + [['foo'], 'g'], + [['foo'], 'f'], + ], + $eventManagerDef->getArgument(1) + ); + + $serviceLocatorDef = $container->getDefinition((string) $eventManagerDef->getArgument(0)); + $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); + $this->assertEquals( + [ + 'a' => new ServiceClosureArgument(new Reference('a')), + 'b' => new ServiceClosureArgument(new Reference('b')), + 'c' => new ServiceClosureArgument(new Reference('c')), + 'd' => new ServiceClosureArgument(new Reference('d')), + 'e' => new ServiceClosureArgument(new Reference('e')), + 'f' => new ServiceClosureArgument(new Reference('f')), + 'g' => new ServiceClosureArgument(new Reference('g')), + 'h' => new ServiceClosureArgument(new Reference('h')), + ], + $serviceLocatorDef->getArgument(0) + ); + } + public function testProcessNoTaggedServices() { $container = $this->createBuilder(true); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterUidTypePassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterUidTypePassTest.php new file mode 100644 index 0000000000000..9b87c051702ee --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterUidTypePassTest.php @@ -0,0 +1,34 @@ +setParameter('doctrine.dbal.connection_factory.types', ['foo' => 'bar']); + (new RegisterUidTypePass())->process($container); + + $expected = [ + 'foo' => 'bar', + 'uuid' => ['class' => UuidType::class], + 'ulid' => ['class' => UlidType::class], + ]; + $this->assertSame($expected, $container->getParameter('doctrine.dbal.connection_factory.types')); + } + + public function testRegisteredDontFail() + { + $container = new ContainerBuilder(); + (new RegisterUidTypePass())->process($container); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php index 4fb8a0a4437d6..17df373369d95 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; /** * @author Fabio B. Silva @@ -38,6 +39,7 @@ protected function setUp(): void 'getObjectManagerElementName', 'getMappingObjectDefaultName', 'getMappingResourceExtension', + 'getMetadataDriverClass', 'load', ]) ->getMock() @@ -48,6 +50,14 @@ protected function setUp(): void ->willReturnCallback(function ($name) { return 'doctrine.orm.'.$name; }); + + $this->extension + ->method('getMappingObjectDefaultName') + ->willReturn('Entity'); + + $this->extension + ->method('getMappingResourceExtension') + ->willReturn('orm'); } public function testFixManagersAutoMappingsWithTwoAutomappings() @@ -170,6 +180,23 @@ public function testFixManagersAutoMappings(array $originalEm1, array $originalE ], $expectedEm2)); } + public function testMappingTypeDetection() + { + $container = $this->createContainer(); + + $reflection = new \ReflectionClass(\get_class($this->extension)); + $method = $reflection->getMethod('detectMappingType'); + $method->setAccessible(true); + + // The ordinary fixtures contain annotation + $mappingType = $method->invoke($this->extension, __DIR__.'/../Fixtures', $container); + $this->assertSame($mappingType, 'annotation'); + + // In the attribute folder, attributes are used + $mappingType = $method->invoke($this->extension, __DIR__.'/../Fixtures/Attribute', $container); + $this->assertSame($mappingType, 'attribute'); + } + public function providerBasicDrivers() { return [ @@ -249,6 +276,73 @@ public function testUnrecognizedCacheDriverException() $this->invokeLoadCacheDriver($objectManager, $container, $cacheName); } + public function providerBundles() + { + yield ['AnnotationsBundle', 'annotation', '/Entity']; + yield ['AttributesBundle', 'attribute', '/Entity']; + yield ['XmlBundle', 'xml', '/Resources/config/doctrine']; + yield ['PhpBundle', 'php', '/Resources/config/doctrine']; + yield ['YamlBundle', 'yml', '/Resources/config/doctrine']; + + yield ['SrcXmlBundle', 'xml', '/Resources/config/doctrine']; + + yield ['NewAnnotationsBundle', 'annotation', \DIRECTORY_SEPARATOR.'src/Entity']; + yield ['NewXmlBundle', 'xml', '/config/doctrine']; + } + + /** + * @dataProvider providerBundles + */ + public function testBundleAutoMapping(string $bundle, string $expectedType, string $dirSuffix) + { + $bundleDir = __DIR__.'/../Fixtures/Bundles/'.$bundle; + $bundleClassName = 'Fixtures\\Bundles\\'.$bundle.'\\'.$bundle; + + if (is_dir($bundleDir.'/src')) { + require_once $bundleDir.'/src/'.$bundle.'.php'; + } else { + require_once $bundleDir.'/'.$bundle.'.php'; + } + + /** @var BundleInterface $bundleClass */ + $bundleClass = new $bundleClassName(); + + $mappingConfig = [ + 'dir' => false, + 'type' => false, + 'prefix' => false, + 'mapping' => true, + 'is_bundle' => true, + ]; + + $this->extension + ->method('getMappingResourceConfigDirectory') + ->willReturnCallback(function ($bundleDir) { + if (null !== $bundleDir && is_dir($bundleDir.'/config/doctrine')) { + return 'config/doctrine'; + } + + return 'Resources/config/doctrine'; + }); + + $container = $this->createContainer([], [$bundle => $bundleClassName]); + + $reflection = new \ReflectionClass(\get_class($this->extension)); + $method = $reflection->getMethod('getMappingDriverBundleConfigDefaults'); + $method->setAccessible(true); + + $this->assertSame( + [ + 'dir' => $bundleClass->getPath().$dirSuffix, + 'type' => $expectedType, + 'prefix' => $bundleClass->getNamespace().'\\Entity', + 'mapping' => true, + 'is_bundle' => true, + ], + $method->invoke($this->extension, $mappingConfig, new \ReflectionClass($bundleClass), $container, $bundleClass->getPath()) + ); + } + protected function invokeLoadCacheDriver(array $objectManager, ContainerBuilder $container, $cacheName) { $method = new \ReflectionMethod($this->extension, 'loadObjectManagerCacheDriver'); @@ -258,11 +352,12 @@ protected function invokeLoadCacheDriver(array $objectManager, ContainerBuilder $method->invokeArgs($this->extension, [$objectManager, $container, $cacheName]); } - protected function createContainer(array $data = []): ContainerBuilder + protected function createContainer(array $data = [], array $extraBundles = []): ContainerBuilder { return new ContainerBuilder(new ParameterBag(array_merge([ - 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], + 'kernel.bundles' => array_merge(['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], $extraBundles), 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, 'kernel.container_class' => 'kernel', 'kernel.project_dir' => __DIR__, ], $data))); diff --git a/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php b/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php similarity index 82% rename from src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php rename to src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php index d3d25c17b275d..be18506ba96ae 100644 --- a/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DoctrineTestHelper.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Bridge\Doctrine\Test; +namespace Symfony\Bridge\Doctrine\Tests; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\ORM\Configuration; @@ -25,35 +25,26 @@ * * @author Bernhard Schussek */ -class DoctrineTestHelper +final class DoctrineTestHelper { /** * Returns an entity manager for testing. - * - * @return EntityManager */ - public static function createTestEntityManager(Configuration $config = null) + public static function createTestEntityManager(Configuration $config = null): EntityManager { if (!\extension_loaded('pdo_sqlite')) { TestCase::markTestSkipped('Extension pdo_sqlite is required.'); } - if (null === $config) { - $config = self::createTestConfiguration(); - } - $params = [ 'driver' => 'pdo_sqlite', 'memory' => true, ]; - return EntityManager::create($params, $config); + return EntityManager::create($params, $config ?? self::createTestConfiguration()); } - /** - * @return Configuration - */ - public static function createTestConfiguration() + public static function createTestConfiguration(): Configuration { $config = new Configuration(); $config->setEntityNamespaces(['SymfonyTestsDoctrine' => 'Symfony\Bridge\Doctrine\Tests\Fixtures']); @@ -65,12 +56,9 @@ public static function createTestConfiguration() return $config; } - /** - * @return Configuration - */ - public static function createTestConfigurationWithXmlLoader() + public static function createTestConfigurationWithXmlLoader(): Configuration { - $config = static::createTestConfiguration(); + $config = self::createTestConfiguration(); $driverChain = new MappingDriverChain(); $driverChain->addDriver( diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Attribute/UuidIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Attribute/UuidIdEntity.php new file mode 100644 index 0000000000000..3d28d4469c1fb --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Attribute/UuidIdEntity.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures\Attribute; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +#[Entity] +class UuidIdEntity +{ + #[Id] + #[Column("uuid")] + protected $id; + + public function __construct($id) + { + $this->id = $id; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php index aa24cd68943dd..f03157637b256 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php @@ -2,9 +2,6 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures; -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Mapping\ClassMetadata; - /** * Class BaseUser. */ @@ -41,14 +38,8 @@ public function getUsername(): string return $this->username; } - public static function loadValidatorMetadata(ClassMetadata $metadata): void + public function getUserIdentifier(): string { - $allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; - - $metadata->addPropertyConstraint('username', new Assert\Length([ - 'min' => 2, - 'max' => 120, - 'groups' => ['Registration'], - ] + $allowEmptyString)); + return $this->username; } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php new file mode 100644 index 0000000000000..e4dfd3e07cc88 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\AnnotationsBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class AnnotationsBundle extends Bundle +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AnnotationsBundle/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AnnotationsBundle/Entity/Person.php new file mode 100644 index 0000000000000..0d7cc91362da3 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AnnotationsBundle/Entity/Person.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\AnnotationsBundle\Entity; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +/** + * @Entity + */ +class Person +{ + /** @Id @Column(type="integer") */ + protected $id; + + /** @Column(type="string") */ + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AttributesBundle/AttributesBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AttributesBundle/AttributesBundle.php new file mode 100644 index 0000000000000..686dbe4e8f3b2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AttributesBundle/AttributesBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\AttributesBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class AttributesBundle extends Bundle +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AttributesBundle/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AttributesBundle/Entity/Person.php new file mode 100644 index 0000000000000..6b445b198457f --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/AttributesBundle/Entity/Person.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\AttributesBundle\Entity; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +#[Entity] +class Person +{ + #[Id, Column(type: 'integer')] + protected $id; + + #[Column(type: 'string')] + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewAnnotationsBundle/src/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewAnnotationsBundle/src/Entity/Person.php new file mode 100644 index 0000000000000..e94a24e1a95c7 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewAnnotationsBundle/src/Entity/Person.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\NewAnnotationsBundle\Entity; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +/** + * @Entity + */ +class Person +{ + /** @Id @Column(type="integer") */ + protected $id; + + /** @Column(type="string") */ + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewAnnotationsBundle/src/NewAnnotationsBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewAnnotationsBundle/src/NewAnnotationsBundle.php new file mode 100644 index 0000000000000..962b6d025ebc8 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewAnnotationsBundle/src/NewAnnotationsBundle.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\NewAnnotationsBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class NewAnnotationsBundle extends Bundle +{ + public function getPath(): string + { + return \dirname(__DIR__); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_label.html.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/config/doctrine/Person.orm.xml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_label.html.php rename to src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/config/doctrine/Person.orm.xml diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/src/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/src/Entity/Person.php new file mode 100644 index 0000000000000..3adfa62aa90fe --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/src/Entity/Person.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\NewXmlBundle\Entity; + +class Person +{ + protected $id; + + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/src/NewXmlBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/src/NewXmlBundle.php new file mode 100644 index 0000000000000..b5abbdb38d45d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/NewXmlBundle/src/NewXmlBundle.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\NewXmlBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class NewXmlBundle extends Bundle +{ + public function getPath(): string + { + return \dirname(__DIR__); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/Entity/Person.php new file mode 100644 index 0000000000000..67937cd3b8bd4 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/Entity/Person.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\PhpBundle\Entity; + +class Person +{ + protected $id; + + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/PhpBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/PhpBundle.php new file mode 100644 index 0000000000000..0fbd8f34dd644 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/PhpBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\PhpBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class PhpBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/Resources/translations/test_default.en.xlf b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/Resources/config/doctrine/Person.orm.php similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/Resources/translations/test_default.en.xlf rename to src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/PhpBundle/Resources/config/doctrine/Person.orm.php diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/Entity/Person.php new file mode 100644 index 0000000000000..445d0d4bd01ab --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/Entity/Person.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\SrcXmlBundle\Entity; + +class Person +{ + protected $id; + + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/Resources/views/base.format.engine b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/Resources/config/doctrine/Person.orm.xml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/Resources/views/base.format.engine rename to src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/Resources/config/doctrine/Person.orm.xml diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/SrcXmlBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/SrcXmlBundle.php new file mode 100644 index 0000000000000..456983db04120 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/SrcXmlBundle/src/SrcXmlBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\SrcXmlBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class SrcXmlBundle extends Bundle +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/Entity/Person.php new file mode 100644 index 0000000000000..83c89773e4911 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/Entity/Person.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\XmlBundle\Entity; + +class Person +{ + protected $id; + + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/Resources/views/controller/base.format.engine b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/Resources/config/doctrine/Person.orm.xml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/Resources/views/controller/base.format.engine rename to src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/Resources/config/doctrine/Person.orm.xml diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/XmlBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/XmlBundle.php new file mode 100644 index 0000000000000..6a69bd7583dd2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/XmlBundle/XmlBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\XmlBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class XmlBundle extends Bundle +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/Entity/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/Entity/Person.php new file mode 100644 index 0000000000000..861cf5b652ab2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/Entity/Person.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\YamlBundle\Entity; + +class Person +{ + protected $id; + + public $name; + + public function __construct($id, $name) + { + $this->id = $id; + $this->name = $name; + } + + public function __toString(): string + { + return (string) $this->name; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/Resources/views/this.is.a.template.format.engine b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/Resources/config/doctrine/Person.orm.yml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/Resources/views/this.is.a.template.format.engine rename to src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/Resources/config/doctrine/Person.orm.yml diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/YamlBundle.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/YamlBundle.php new file mode 100644 index 0000000000000..415db47843d9d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Bundles/YamlBundle/YamlBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Fixtures\Bundles\YamlBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class YamlBundle extends Bundle +{ +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php index 06f8674e56d66..d6aee2d18b0b1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php @@ -14,7 +14,6 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Mapping\ClassMetadata; /** * @ORM\Entity @@ -37,11 +36,13 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity /** * @ORM\Column(length=20) + * @Assert\Length(min=5) */ public $mergedMaxLength; /** * @ORM\Column(length=20) + * @Assert\Length(min=1, max=10) */ public $alreadyMappedMaxLength; @@ -74,12 +75,4 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity * @Assert\DisableAutoMapping */ public $noAutoMapping; - - public static function loadValidatorMetadata(ClassMetadata $metadata): void - { - $allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; - - $metadata->addPropertyConstraint('mergedMaxLength', new Assert\Length(['min' => 5] + $allowEmptyString)); - $metadata->addPropertyConstraint('alreadyMappedMaxLength', new Assert\Length(['min' => 1, 'max' => 10] + $allowEmptyString)); - } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php index d01148f3b018c..217f90374b276 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php @@ -18,20 +18,16 @@ class StringWrapperType extends StringType { /** * {@inheritdoc} - * - * @return mixed */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue($value, AbstractPlatform $platform): mixed { return $value instanceof StringWrapper ? $value->getString() : null; } /** * {@inheritdoc} - * - * @return mixed */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue($value, AbstractPlatform $platform): mixed { return new StringWrapper($value); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UlidIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UlidIdEntity.php new file mode 100644 index 0000000000000..3ee909fe4bfc5 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UlidIdEntity.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +/** @Entity */ +class UlidIdEntity +{ + /** @Id @Column(type="ulid") */ + protected $id; + + public function __construct($id) + { + $this->id = $id; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php index c5cbc662fc1d1..b8b10094dd6bd 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php @@ -14,10 +14,11 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; /** @Entity */ -class User implements UserInterface +class User implements UserInterface, PasswordAuthenticatedUserInterface { /** @Id @Column(type="integer") */ protected $id1; @@ -43,11 +44,12 @@ public function getPassword(): ?string { } - public function getSalt(): ?string + public function getUsername(): string { + return $this->name; } - public function getUsername(): string + public function getUserIdentifier(): string { return $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php index 004eec8201336..98b3c96d9b3a7 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php @@ -19,15 +19,18 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; -use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; +use Symfony\Component\Form\Exception\LogicException; /** * @author Bernhard Schussek */ class DoctrineChoiceLoaderTest extends TestCase { + use ExpectDeprecationTrait; + /** * @var MockObject&ChoiceListFactoryInterface */ @@ -99,6 +102,10 @@ protected function setUp(): void ->method('getClassMetadata') ->with($this->class) ->willReturn(new ClassMetadata($this->class)); + $this->repository->expects($this->any()) + ->method('findAll') + ->willReturn([$this->obj1, $this->obj2, $this->obj3]) + ; } public function testLoadChoiceList() @@ -187,6 +194,9 @@ public function testLoadValuesForChoicesDoesNotLoadIfEmptyChoices() public function testLoadValuesForChoicesDoesNotLoadIfSingleIntId() { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.'); + $loader = new DoctrineChoiceLoader( $this->om, $this->class, @@ -204,7 +214,7 @@ public function testLoadValuesForChoicesDoesNotLoadIfSingleIntId() $this->assertSame(['2'], $loader->loadValuesForChoices([$this->obj2])); } - public function testLoadValuesForChoicesLoadsIfSingleIntIdAndValueGiven() + public function testLoadValuesForChoicesDoesNotLoadIfSingleIntIdAndValueGiven() { $loader = new DoctrineChoiceLoader( $this->om, @@ -215,7 +225,7 @@ public function testLoadValuesForChoicesLoadsIfSingleIntIdAndValueGiven() $choices = [$this->obj1, $this->obj2, $this->obj3]; $value = function (\stdClass $object) { return $object->name; }; - $this->repository->expects($this->once()) + $this->repository->expects($this->never()) ->method('findAll') ->willReturn($choices); @@ -253,8 +263,7 @@ public function testLoadChoicesForValues() { $loader = new DoctrineChoiceLoader( $this->om, - $this->class, - $this->idReader + $this->class ); $choices = [$this->obj1, $this->obj2, $this->obj3]; @@ -284,7 +293,37 @@ public function testLoadChoicesForValuesDoesNotLoadIfEmptyValues() $this->assertSame([], $loader->loadChoicesForValues([])); } - public function testLoadChoicesForValuesLoadsOnlyChoicesIfSingleIntId() + public function testLegacyLoadChoicesForValuesLoadsOnlyChoicesIfValueUseIdReader() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.'); + + $loader = new DoctrineChoiceLoader( + $this->om, + $this->class, + $this->idReader, + $this->objectLoader + ); + + $choices = [$this->obj2, $this->obj3]; + + $this->idReader->expects($this->any()) + ->method('getIdField') + ->willReturn('idField'); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->objectLoader->expects($this->never()) + ->method('getEntitiesByIds'); + + $this->assertSame( + [4 => $this->obj3, 7 => $this->obj2], + $loader->loadChoicesForValues([4 => '3', 7 => '2'] + )); + } + + public function testLoadChoicesForValuesLoadsOnlyChoicesIfValueUseIdReader() { $loader = new DoctrineChoiceLoader( $this->om, @@ -316,7 +355,7 @@ public function testLoadChoicesForValuesLoadsOnlyChoicesIfSingleIntId() $this->assertSame( [4 => $this->obj3, 7 => $this->obj2], - $loader->loadChoicesForValues([4 => '3', 7 => '2'] + $loader->loadChoicesForValues([4 => '3', 7 => '2'], [$this->idReader, 'getIdValue'] )); } @@ -375,87 +414,17 @@ public function testLoadChoicesForValuesLoadsOnlyChoicesIfValueIsIdReader() $this->assertSame([$this->obj2], $loader->loadChoicesForValues(['2'], $value)); } - /** - * @group legacy - * - * @expectedDeprecation Not explicitly passing an instance of "Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader" to "Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader" when it can optimize single id entity "%s" has been deprecated in 4.3 and will not apply any optimization in 5.0. - */ - public function testLoaderWithoutIdReaderCanBeOptimized() - { - $obj1 = new SingleIntIdEntity('1', 'one'); - $obj2 = new SingleIntIdEntity('2', 'two'); - - $metadata = $this->createMock(ClassMetadata::class); - $metadata->expects($this->once()) - ->method('getIdentifierFieldNames') - ->willReturn(['idField']) - ; - $metadata->expects($this->any()) - ->method('getIdentifierValues') - ->willReturnCallback(function ($obj) use ($obj1, $obj2) { - if ($obj === $obj1) { - return ['idField' => '1']; - } - if ($obj === $obj2) { - return ['idField' => '2']; - } - - return null; - }) - ; - - $this->om = $this->createMock(ObjectManager::class); - $this->om->expects($this->once()) - ->method('getClassMetadata') - ->with(SingleIntIdEntity::class) - ->willReturn($metadata) - ; - $this->om->expects($this->any()) - ->method('contains') - ->with($this->isInstanceOf(SingleIntIdEntity::class)) - ->willReturn(true) - ; - - $loader = new DoctrineChoiceLoader( - $this->om, - SingleIntIdEntity::class, - null, - $this->objectLoader - ); - - $choices = [$obj1, $obj2]; - - $this->repository->expects($this->never()) - ->method('findAll'); - - $this->objectLoader->expects($this->once()) - ->method('getEntitiesByIds') - ->with('idField', ['1']) - ->willReturn($choices); - - $this->assertSame([$obj1], $loader->loadChoicesForValues(['1'])); - } - - /** - * @group legacy - * - * @deprecationMessage Passing an instance of "Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader" to "Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader" with an entity class "stdClass" that has a composite id is deprecated since Symfony 4.3 and will throw an exception in 5.0. - */ public function testPassingIdReaderWithoutSingleIdEntity() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The second argument `$idReader` of "Symfony\\Bridge\\Doctrine\\Form\\ChoiceList\\DoctrineChoiceLoader::__construct" must be null when the query cannot be optimized because of composite id fields.'); + $idReader = $this->createMock(IdReader::class); $idReader->expects($this->once()) ->method('isSingleId') ->willReturn(false) ; - $loader = new DoctrineChoiceLoader( - $this->om, - $this->class, - $idReader, - $this->objectLoader - ); - - $this->assertInstanceOf(DoctrineChoiceLoader::class, $loader); + new DoctrineChoiceLoader($this->om, $this->class, $idReader, $this->objectLoader); } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php index 8bb0f256977a5..7d253dc59b85d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php @@ -12,13 +12,26 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\GuidType; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Version; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Types\UlidType; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Uid\Uuid; class ORMQueryBuilderLoaderTest extends TestCase { + protected function tearDown(): void + { + if (Type::hasType('uuid')) { + Type::overrideType('uuid', GuidType::class); + } + } + public function testIdentifierTypeIsStringArray() { $this->checkIdentifierType('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity', Connection::PARAM_STR_ARRAY); @@ -131,6 +144,86 @@ public function testFilterEmptyUuids($entityClass) $loader->getEntitiesByIds('id', ['71c5fd46-3f16-4abb-bad7-90ac1e654a2d', '', 'b98e8e11-2897-44df-ad24-d2627eb7f499']); } + /** + * @dataProvider provideUidEntityClasses + */ + public function testFilterUid($entityClass) + { + if (Type::hasType('uuid')) { + Type::overrideType('uuid', UuidType::class); + } else { + Type::addType('uuid', UuidType::class); + } + if (!Type::hasType('ulid')) { + Type::addType('ulid', UlidType::class); + } + + $em = DoctrineTestHelper::createTestEntityManager(); + + $query = $this->getMockBuilder(\QueryMock::class) + ->setMethods(['setParameter', 'getResult', 'getSql', '_doExecute']) + ->getMock(); + + $query + ->method('getResult') + ->willReturn([]); + + $query->expects($this->once()) + ->method('setParameter') + ->with('ORMQueryBuilderLoader_getEntitiesByIds_id', [Uuid::fromString('71c5fd46-3f16-4abb-bad7-90ac1e654a2d')->toBinary(), Uuid::fromString('b98e8e11-2897-44df-ad24-d2627eb7f499')->toBinary()], Connection::PARAM_STR_ARRAY) + ->willReturn($query); + + $qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) + ->setConstructorArgs([$em]) + ->setMethods(['getQuery']) + ->getMock(); + + $qb->expects($this->once()) + ->method('getQuery') + ->willReturn($query); + + $qb->select('e') + ->from($entityClass, 'e'); + + $loader = new ORMQueryBuilderLoader($qb); + $loader->getEntitiesByIds('id', ['71c5fd46-3f16-4abb-bad7-90ac1e654a2d', '', 'b98e8e11-2897-44df-ad24-d2627eb7f499']); + } + + /** + * @dataProvider provideUidEntityClasses + */ + public function testUidThrowProperException($entityClass) + { + if (Type::hasType('uuid')) { + Type::overrideType('uuid', UuidType::class); + } else { + Type::addType('uuid', UuidType::class); + } + if (!Type::hasType('ulid')) { + Type::addType('ulid', UlidType::class); + } + + $em = DoctrineTestHelper::createTestEntityManager(); + + $qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) + ->setConstructorArgs([$em]) + ->setMethods(['getQuery']) + ->getMock(); + + $qb->expects($this->never()) + ->method('getQuery'); + + $qb->select('e') + ->from($entityClass, 'e'); + + $loader = new ORMQueryBuilderLoader($qb); + + $this->expectException(TransformationFailedException::class); + $this->expectExceptionMessageMatches('/^Failed to transform "hello" into "(uuid|ulid)"\.$/'); + + $loader->getEntitiesByIds('id', ['hello']); + } + public function testEmbeddedIdentifierName() { if (Version::compare('2.5.0') > 0) { @@ -176,4 +269,12 @@ public function provideGuidEntityClasses() ['Symfony\Bridge\Doctrine\Tests\Fixtures\UuidIdEntity'], ]; } + + public function provideUidEntityClasses() + { + return [ + ['Symfony\Bridge\Doctrine\Tests\Fixtures\UuidIdEntity'], + ['Symfony\Bridge\Doctrine\Tests\Fixtures\UlidIdEntity'], + ]; + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php index e003a20ee6b56..652f9d67ebe18 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php @@ -11,16 +11,44 @@ namespace Symfony\Bridge\Doctrine\Tests\Form; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; use Symfony\Component\Form\Guess\ValueGuess; class DoctrineOrmTypeGuesserTest extends TestCase { + /** + * @dataProvider requiredType + */ + public function testTypeGuesser(string $type, $expected) + { + $classMetadata = $this->createMock(ClassMetadata::class); + $classMetadata->fieldMappings['field'] = true; + $classMetadata->expects($this->once())->method('getTypeOfField')->with('field')->willReturn($type); + + $this->assertEquals($expected, $this->getGuesser($classMetadata)->guessType('TestEntity', 'field')); + } + + public function requiredType() + { + yield [Types::DATE_IMMUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE)]; + yield [Types::DATE_MUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::HIGH_CONFIDENCE)]; + + yield [Types::TIME_IMMUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE)]; + yield [Types::TIME_MUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', [], Guess::HIGH_CONFIDENCE)]; + + yield [Types::DATETIME_IMMUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE)]; + yield [Types::DATETIMETZ_IMMUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE)]; + yield [Types::DATETIME_MUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', [], Guess::HIGH_CONFIDENCE)]; + yield [Types::DATETIMETZ_MUTABLE, new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', [], Guess::HIGH_CONFIDENCE)]; + } + /** * @dataProvider requiredProvider */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php index 4b0a93884be3c..bdc6b5dcab91e 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php @@ -14,7 +14,7 @@ use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Component\Form\Extension\Core\CoreExtension; use Symfony\Component\Form\Test\FormPerformanceTestCase; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index cb7eeed32bbf2..d1c2c04b88956 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -20,7 +20,7 @@ use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Bridge\Doctrine\Form\Type\EntityType; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity; @@ -29,7 +29,7 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; -use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\Exception\RuntimeException; @@ -1241,13 +1241,13 @@ public function testLoaderCaching() 'property3' => 2, ]); - $choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader'); - $choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader'); - $choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader'); + $choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list'); - $this->assertInstanceOf(ChoiceLoaderInterface::class, $choiceLoader1); - $this->assertSame($choiceLoader1, $choiceLoader2); - $this->assertSame($choiceLoader1, $choiceLoader3); + $this->assertInstanceOf(LazyChoiceList::class, $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); } public function testLoaderCachingWithParameters() @@ -1301,13 +1301,13 @@ public function testLoaderCachingWithParameters() 'property3' => 2, ]); - $choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader'); - $choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader'); - $choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader'); + $choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list'); - $this->assertInstanceOf(ChoiceLoaderInterface::class, $choiceLoader1); - $this->assertSame($choiceLoader1, $choiceLoader2); - $this->assertSame($choiceLoader1, $choiceLoader3); + $this->assertInstanceOf(LazyChoiceList::class, $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); } /** diff --git a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/EntityManager.php b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/EntityManager.php new file mode 100644 index 0000000000000..22667a6daad4d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/EntityManager.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\IdGenerator; + +use Doctrine\ORM\EntityManager as DoctrineEntityManager; + +class EntityManager extends DoctrineEntityManager +{ + public function __construct() + { + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php new file mode 100644 index 0000000000000..957ac0f60aeb0 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UlidGeneratorTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\IdGenerator; + +use Doctrine\ORM\Mapping\Entity; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator; +use Symfony\Component\Uid\Factory\UlidFactory; +use Symfony\Component\Uid\Ulid; + +class UlidGeneratorTest extends TestCase +{ + public function testUlidCanBeGenerated() + { + $em = new EntityManager(); + $generator = new UlidGenerator(); + $ulid = $generator->generate($em, new Entity()); + + $this->assertInstanceOf(Ulid::class, $ulid); + $this->assertTrue(Ulid::isValid($ulid)); + } + + /** + * @requires function \Symfony\Component\Uid\Factory\UlidFactory::create + */ + public function testUlidFactory() + { + $ulid = new Ulid('00000000000000000000000000'); + $em = new EntityManager(); + $factory = $this->createMock(UlidFactory::class); + $factory->expects($this->any()) + ->method('create') + ->willReturn($ulid); + $generator = new UlidGenerator($factory); + + $this->assertSame($ulid, $generator->generate($em, new Entity())); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php new file mode 100644 index 0000000000000..34367b0bd7213 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/IdGenerator/UuidGeneratorTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\IdGenerator; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; +use Symfony\Component\Uid\Factory\UuidFactory; +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Uid\UuidV6; + +/** + * @requires function \Symfony\Component\Uid\Factory\UuidFactory::create + */ +class UuidGeneratorTest extends TestCase +{ + public function testUuidCanBeGenerated() + { + $em = new EntityManager(); + $generator = new UuidGenerator(); + $uuid = $generator->generate($em, new Entity()); + + $this->assertInstanceOf(Uuid::class, $uuid); + } + + public function testCustomUuidfactory() + { + $uuid = new UuidV4(); + $em = new EntityManager(); + $factory = $this->createMock(UuidFactory::class); + $factory->expects($this->any()) + ->method('create') + ->willReturn($uuid); + $generator = new UuidGenerator($factory); + + $this->assertSame($uuid, $generator->generate($em, new Entity())); + } + + public function testUuidfactory() + { + $em = new EntityManager(); + $generator = new UuidGenerator(); + $this->assertInstanceOf(UuidV6::class, $generator->generate($em, new Entity())); + + $generator = $generator->randomBased(); + $this->assertInstanceOf(UuidV4::class, $generator->generate($em, new Entity())); + + $generator = $generator->timeBased(); + $this->assertInstanceOf(UuidV6::class, $generator->generate($em, new Entity())); + + $generator = $generator->nameBased('prop1', Uuid::NAMESPACE_OID); + $this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '3'), $generator->generate($em, new Entity())); + + $generator = $generator->nameBased('prop2', Uuid::NAMESPACE_OID); + $this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '2'), $generator->generate($em, new Entity())); + + $generator = $generator->nameBased('getProp4', Uuid::NAMESPACE_OID); + $this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '4'), $generator->generate($em, new Entity())); + + $factory = new UuidFactory(6, 6, 5, 5, null, Uuid::NAMESPACE_OID); + $generator = new UuidGenerator($factory); + $generator = $generator->nameBased('prop1'); + $this->assertEquals(Uuid::v5(new Uuid(Uuid::NAMESPACE_OID), '3'), $generator->generate($em, new Entity())); + } +} + +class Entity +{ + public $prop1 = 1; + public $prop2 = 2; + + public function prop1() + { + return 3; + } + + public function getProp4() + { + return 4; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php new file mode 100644 index 0000000000000..626c19eb4ceae --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineOpenTransactionLoggerMiddlewareTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Messenger; + +use Doctrine\DBAL\Connection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; +use Psr\Log\AbstractLogger; +use Symfony\Bridge\Doctrine\Messenger\DoctrineOpenTransactionLoggerMiddleware; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; + +class DoctrineOpenTransactionLoggerMiddlewareTest extends MiddlewareTestCase +{ + private $logger; + private $connection; + private $entityManager; + private $middleware; + + protected function setUp(): void + { + $this->logger = new class() extends AbstractLogger { + public $logs = []; + + public function log($level, $message, $context = []): void + { + $this->logs[$level][] = $message; + } + }; + + $this->connection = $this->createMock(Connection::class); + + $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->entityManager->method('getConnection')->willReturn($this->connection); + + $managerRegistry = $this->createMock(ManagerRegistry::class); + $managerRegistry->method('getManager')->willReturn($this->entityManager); + + $this->middleware = new DoctrineOpenTransactionLoggerMiddleware($managerRegistry, null, $this->logger); + } + + public function testMiddlewareWrapsInTransactionAndFlushes() + { + $this->connection->expects($this->exactly(1)) + ->method('isTransactionActive') + ->will($this->onConsecutiveCalls(true, true, false)) + ; + + $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); + + $this->assertSame(['error' => ['A handler opened a transaction but did not close it.']], $this->logger->logs); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php index be63ef923dfbc..6c7bf67bc08af 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php @@ -12,8 +12,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Messenger; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception as DBALException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Messenger\DoctrinePingConnectionMiddleware; @@ -50,7 +49,7 @@ public function testMiddlewarePingOk() { $this->connection->expects($this->once()) ->method('getDatabasePlatform') - ->will($this->throwException(class_exists(Exception::class) ? new Exception() : new DBALException())); + ->will($this->throwException(new DBALException())); $this->connection->expects($this->once()) ->method('close') @@ -69,7 +68,7 @@ public function testMiddlewarePingResetEntityManager() { $this->connection->expects($this->once()) ->method('getDatabasePlatform') - ->will($this->throwException(class_exists(Exception::class) ? new Exception() : new DBALException())); + ->will($this->throwException(new DBALException())); $this->entityManager->expects($this->once()) ->method('isOpen') diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index b1e327968242a..5d0a09f5f906a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -13,13 +13,12 @@ use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Type as DBALType; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Tools\Setup; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; -use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy210; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineEnum; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineGeneratedValue; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation; @@ -32,7 +31,7 @@ */ class DoctrineExtractorTest extends TestCase { - private function createExtractor(bool $legacy = false) + private function createExtractor() { $config = Setup::createAnnotationMetadataConfiguration([__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'], true); $entityManager = EntityManager::create(['driver' => 'pdo_sqlite'], $config); @@ -42,20 +41,10 @@ private function createExtractor(bool $legacy = false) $entityManager->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('custom_foo', 'foo'); } - return new DoctrineExtractor($legacy ? $entityManager->getMetadataFactory() : $entityManager); + return new DoctrineExtractor($entityManager); } public function testGetProperties() - { - $this->doTestGetProperties(false); - } - - public function testLegacyGetProperties() - { - $this->doTestGetProperties(true); - } - - private function doTestGetProperties(bool $legacy) { // Fields $expected = [ @@ -72,12 +61,9 @@ private function doTestGetProperties(bool $legacy) 'binary', 'customFoo', 'bigint', + 'json', ]; - if (class_exists(Types::class)) { - $expected[] = 'json'; - } - // Associations $expected = array_merge($expected, [ 'foo', @@ -94,21 +80,11 @@ private function doTestGetProperties(bool $legacy) $this->assertEquals( $expected, - $this->createExtractor($legacy)->getProperties(!class_exists(Types::class) ? 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy' : DoctrineDummy210::class) + $this->createExtractor()->getProperties(DoctrineDummy::class) ); } public function testTestGetPropertiesWithEmbedded() - { - $this->doTestGetPropertiesWithEmbedded(false); - } - - public function testLegacyTestGetPropertiesWithEmbedded() - { - $this->doTestGetPropertiesWithEmbedded(true); - } - - private function doTestGetPropertiesWithEmbedded(bool $legacy) { if (!class_exists(\Doctrine\ORM\Mapping\Embedded::class)) { $this->markTestSkipped('@Embedded is not available in Doctrine ORM lower than 2.5.'); @@ -119,7 +95,7 @@ private function doTestGetPropertiesWithEmbedded(bool $legacy) 'id', 'embedded', ], - $this->createExtractor($legacy)->getProperties('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineWithEmbedded') + $this->createExtractor()->getProperties('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineWithEmbedded') ); } @@ -128,33 +104,10 @@ private function doTestGetPropertiesWithEmbedded(bool $legacy) */ public function testExtract($property, array $type = null) { - $this->doTestExtract(false, $property, $type); - } - - /** - * @dataProvider typesProvider - */ - public function testLegacyExtract($property, array $type = null) - { - $this->doTestExtract(true, $property, $type); - } - - private function doTestExtract(bool $legacy, $property, array $type = null) - { - $this->assertEquals($type, $this->createExtractor($legacy)->getTypes(!class_exists(Types::class) ? 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy' : DoctrineDummy210::class, $property, [])); + $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property, [])); } public function testExtractWithEmbedded() - { - $this->doTestExtractWithEmbedded(false); - } - - public function testLegacyExtractWithEmbedded() - { - $this->doTestExtractWithEmbedded(true); - } - - private function doTestExtractWithEmbedded(bool $legacy) { if (!class_exists(\Doctrine\ORM\Mapping\Embedded::class)) { $this->markTestSkipped('@Embedded is not available in Doctrine ORM lower than 2.5.'); @@ -166,7 +119,7 @@ private function doTestExtractWithEmbedded(bool $legacy) 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineEmbeddable' )]; - $actualTypes = $this->createExtractor($legacy)->getTypes( + $actualTypes = $this->createExtractor()->getTypes( 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineWithEmbedded', 'embedded', [] @@ -273,41 +226,17 @@ public function typesProvider() ['json', null], ]; - if (class_exists(Types::class)) { - $provider[] = ['json', null]; - } - return $provider; } public function testGetPropertiesCatchException() { - $this->doTestGetPropertiesCatchException(false); - } - - public function testLegacyGetPropertiesCatchException() - { - $this->doTestGetPropertiesCatchException(true); - } - - private function doTestGetPropertiesCatchException(bool $legacy) - { - $this->assertNull($this->createExtractor($legacy)->getProperties('Not\Exist')); + $this->assertNull($this->createExtractor()->getProperties('Not\Exist')); } public function testGetTypesCatchException() { - return $this->doTestGetTypesCatchException(false); - } - - public function testLegacyGetTypesCatchException() - { - return $this->doTestGetTypesCatchException(true); - } - - private function doTestGetTypesCatchException(bool $legacy) - { - $this->assertNull($this->createExtractor($legacy)->getTypes('Not\Exist', 'baz')); + $this->assertNull($this->createExtractor()->getTypes('Not\Exist', 'baz')); } public function testGeneratedValueNotWritable() diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php index 67d61f2abfd3d..8190540e27566 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php @@ -138,6 +138,11 @@ class DoctrineDummy */ protected $indexedBuz; + /** + * @Column(type="json", nullable=true) + */ + private $json; + /** * @OneToMany(targetEntity="DoctrineRelation", mappedBy="dummyRelation", indexBy="gen_value_col_id", orphanRemoval=true) */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php deleted file mode 100644 index d3916143deab7..0000000000000 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; - -use Doctrine\ORM\Mapping\Column; -use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\Id; -use Doctrine\ORM\Mapping\ManyToMany; -use Doctrine\ORM\Mapping\ManyToOne; -use Doctrine\ORM\Mapping\OneToMany; - -/** - * @Entity - */ -final class DoctrineDummy210 extends DoctrineDummy -{ - /** - * @Column(type="json", nullable=true) - */ - private $json; -} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php index d7dbb1eeb41f9..312e6c51b69bf 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php @@ -20,9 +20,6 @@ */ class DoctrineFooType extends Type { - /** - * Type name. - */ private const NAME = 'foo'; /** @@ -43,16 +40,14 @@ public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $pla /** * {@inheritdoc} - * - * @return mixed */ - public function convertToDatabaseValue($value, AbstractPlatform $platform) + public function convertToDatabaseValue($value, AbstractPlatform $platform): mixed { if (null === $value) { return null; } if (!$value instanceof Foo) { - throw new ConversionException(sprintf('Expected "%s", got "%s"', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', \gettype($value))); + throw new ConversionException(sprintf('Expected "%s", got "%s"', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', get_debug_type($value))); } return $foo->bar; @@ -60,10 +55,8 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) /** * {@inheritdoc} - * - * @return mixed */ - public function convertToPHPValue($value, AbstractPlatform $platform) + public function convertToPHPValue($value, AbstractPlatform $platform): mixed { if (null === $value) { return null; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml index ddb8a13bc1fcc..bf64b92ca484d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml +++ b/src/Symfony/Bridge/Doctrine/Tests/Resources/validator/BaseUser.xml @@ -9,6 +9,11 @@ + + + + + diff --git a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriberTest.php new file mode 100644 index 0000000000000..0a65b6e6bc720 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/DoctrineDbalCacheAdapterSchemaSubscriberTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\SchemaListener; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\SchemaListener\DoctrineDbalCacheAdapterSchemaSubscriber; +use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; + +class DoctrineDbalCacheAdapterSchemaSubscriberTest extends TestCase +{ + public function testPostGenerateSchema() + { + $schema = new Schema(); + $dbalConnection = $this->createMock(Connection::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('getConnection') + ->willReturn($dbalConnection); + + $event = new GenerateSchemaEventArgs($entityManager, $schema); + + $dbalAdapter = $this->createMock(DoctrineDbalAdapter::class); + $dbalAdapter->expects($this->once()) + ->method('configureSchema') + ->with($schema, $dbalConnection); + + $subscriber = new DoctrineDbalCacheAdapterSchemaSubscriber([$dbalAdapter]); + $subscriber->postGenerateSchema($event); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/MessengerTransportDoctrineSchemaSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/MessengerTransportDoctrineSchemaSubscriberTest.php new file mode 100644 index 0000000000000..ff4ab2c27a19c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/MessengerTransportDoctrineSchemaSubscriberTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\SchemaListener; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Event\SchemaCreateTableEventArgs; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\SchemaListener\MessengerTransportDoctrineSchemaSubscriber; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport; +use Symfony\Component\Messenger\Transport\TransportInterface; + +class MessengerTransportDoctrineSchemaSubscriberTest extends TestCase +{ + public function testPostGenerateSchema() + { + $schema = new Schema(); + $dbalConnection = $this->createMock(Connection::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('getConnection') + ->willReturn($dbalConnection); + $event = new GenerateSchemaEventArgs($entityManager, $schema); + + $doctrineTransport = $this->createMock(DoctrineTransport::class); + $doctrineTransport->expects($this->once()) + ->method('configureSchema') + ->with($schema, $dbalConnection); + $otherTransport = $this->createMock(TransportInterface::class); + $otherTransport->expects($this->never()) + ->method($this->anything()); + + $subscriber = new MessengerTransportDoctrineSchemaSubscriber([$doctrineTransport, $otherTransport]); + $subscriber->postGenerateSchema($event); + } + + public function testOnSchemaCreateTable() + { + $platform = $this->createMock(AbstractPlatform::class); + $table = new Table('queue_table'); + $event = new SchemaCreateTableEventArgs($table, [], [], $platform); + + $otherTransport = $this->createMock(TransportInterface::class); + $otherTransport->expects($this->never()) + ->method($this->anything()); + + $doctrineTransport = $this->createMock(DoctrineTransport::class); + $doctrineTransport->expects($this->once()) + ->method('getExtraSetupSqlForTable') + ->with($table) + ->willReturn(['ALTER TABLE pizza ADD COLUMN extra_cheese boolean']); + + // we use the platform to generate the full create table sql + $platform->expects($this->once()) + ->method('getCreateTableSQL') + ->with($table) + ->willReturn('CREATE TABLE pizza (id integer NOT NULL)'); + + $subscriber = new MessengerTransportDoctrineSchemaSubscriber([$otherTransport, $doctrineTransport]); + $subscriber->onSchemaCreateTable($event); + $this->assertTrue($event->isDefaultPrevented()); + $this->assertSame([ + 'CREATE TABLE pizza (id integer NOT NULL)', + 'ALTER TABLE pizza ADD COLUMN extra_cheese boolean', + ], $event->getSql()); + } + + public function testOnSchemaCreateTableNoExtraSql() + { + $platform = $this->createMock(AbstractPlatform::class); + $table = new Table('queue_table'); + $event = new SchemaCreateTableEventArgs($table, [], [], $platform); + + $doctrineTransport = $this->createMock(DoctrineTransport::class); + $doctrineTransport->expects($this->once()) + ->method('getExtraSetupSqlForTable') + ->willReturn([]); + + $platform->expects($this->never()) + ->method('getCreateTableSQL'); + + $subscriber = new MessengerTransportDoctrineSchemaSubscriber([$doctrineTransport]); + $subscriber->onSchemaCreateTable($event); + $this->assertFalse($event->isDefaultPrevented()); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php index 6e406b06b76af..4e75f41cb688a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php @@ -56,6 +56,56 @@ public function testDeleteToken() $provider->loadTokenBySeries('someSeries'); } + public function testVerifyOutdatedTokenAfterParallelRequest() + { + $provider = $this->bootstrapProvider(); + $series = base64_encode(random_bytes(64)); + $oldValue = 'oldValue'; + $newValue = 'newValue'; + + // setup existing token + $token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51')); + $provider->createNewToken($token); + + // new request comes in requiring remember-me auth, which updates the token + $provider->updateExistingToken($token, $newValue, new \DateTime('-5 seconds')); + $provider->updateToken($series, $newValue, new \DateTime('-5 seconds')); + + // parallel request comes in with the old remember-me cookie and session, which also requires reauth + $token = $provider->loadTokenBySeries($series); + $this->assertEquals($newValue, $token->getTokenValue()); + + // new token is valid + $this->assertTrue($provider->verifyToken($token, $newValue)); + // old token is still valid + $this->assertTrue($provider->verifyToken($token, $oldValue)); + } + + public function testVerifyOutdatedTokenAfterParallelRequestFailsAfter60Seconds() + { + $provider = $this->bootstrapProvider(); + $series = base64_encode(random_bytes(64)); + $oldValue = 'oldValue'; + $newValue = 'newValue'; + + // setup existing token + $token = new PersistentToken('someClass', 'someUser', $series, $oldValue, new \DateTime('2013-01-26T18:23:51')); + $provider->createNewToken($token); + + // new request comes in requiring remember-me auth, which updates the token + $provider->updateExistingToken($token, $newValue, new \DateTime('-61 seconds')); + $provider->updateToken($series, $newValue, new \DateTime('-5 seconds')); + + // parallel request comes in with the old remember-me cookie and session, which also requires reauth + $token = $provider->loadTokenBySeries($series); + $this->assertEquals($newValue, $token->getTokenValue()); + + // new token is valid + $this->assertTrue($provider->verifyToken($token, $newValue)); + // old token is not valid anymore after 60 seconds + $this->assertFalse($provider->verifyToken($token, $oldValue)); + } + /** * @return DoctrineTokenProvider */ diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php index 845515b901155..20d1e487a23d2 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php @@ -19,9 +19,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider; use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\User; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -59,7 +60,7 @@ public function testLoadUserByUsername() $provider = new EntityUserProvider($this->getManager($em), 'Symfony\Bridge\Doctrine\Tests\Fixtures\User', 'name'); - $this->assertSame($user, $provider->loadUserByUsername('user1')); + $this->assertSame($user, $provider->loadUserByIdentifier('user1')); } public function testLoadUserByUsernameWithUserLoaderRepositoryAndWithoutProperty() @@ -69,7 +70,7 @@ public function testLoadUserByUsernameWithUserLoaderRepositoryAndWithoutProperty $repository = $this->createMock(UserLoaderRepository::class); $repository ->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with('user1') ->willReturn($user); @@ -81,7 +82,7 @@ public function testLoadUserByUsernameWithUserLoaderRepositoryAndWithoutProperty ->willReturn($repository); $provider = new EntityUserProvider($this->getManager($em), 'Symfony\Bridge\Doctrine\Tests\Fixtures\User'); - $this->assertSame($user, $provider->loadUserByUsername('user1')); + $this->assertSame($user, $provider->loadUserByIdentifier('user1')); } public function testLoadUserByUsernameWithNonUserLoaderRepositoryAndWithoutProperty() @@ -97,7 +98,7 @@ public function testLoadUserByUsernameWithNonUserLoaderRepositoryAndWithoutPrope $em->flush(); $provider = new EntityUserProvider($this->getManager($em), 'Symfony\Bridge\Doctrine\Tests\Fixtures\User'); - $provider->loadUserByUsername('user1'); + $provider->loadUserByIdentifier('user1'); } public function testRefreshUserRequiresId() @@ -125,7 +126,7 @@ public function testRefreshInvalidUser() $provider = new EntityUserProvider($this->getManager($em), 'Symfony\Bridge\Doctrine\Tests\Fixtures\User', 'name'); $user2 = new User(1, 2, 'user2'); - $this->expectException(UsernameNotFoundException::class); + $this->expectException(UserNotFoundException::class); $this->expectExceptionMessage('User with id {"id1":1,"id2":2} not found'); $provider->refreshUser($user2); @@ -152,7 +153,7 @@ public function testLoadUserByUserNameShouldLoadUserWhenProperInterfaceProvided( { $repository = $this->createMock(UserLoaderRepository::class); $repository->expects($this->once()) - ->method('loadUserByUsername') + ->method('loadUserByIdentifier') ->with('name') ->willReturn( $this->createMock(UserInterface::class) @@ -163,7 +164,7 @@ public function testLoadUserByUserNameShouldLoadUserWhenProperInterfaceProvided( 'Symfony\Bridge\Doctrine\Tests\Fixtures\User' ); - $provider->loadUserByUsername('name'); + $provider->loadUserByIdentifier('name'); } public function testLoadUserByUserNameShouldDeclineInvalidInterface() @@ -176,7 +177,7 @@ public function testLoadUserByUserNameShouldDeclineInvalidInterface() 'Symfony\Bridge\Doctrine\Tests\Fixtures\User' ); - $provider->loadUserByUsername('name'); + $provider->loadUserByIdentifier('name'); } public function testPasswordUpgrades() @@ -230,11 +231,10 @@ private function createSchema($em) abstract class UserLoaderRepository implements ObjectRepository, UserLoaderInterface { + abstract public function loadUserByIdentifier(string $identifier): ?UserInterface; } abstract class PasswordUpgraderRepository implements ObjectRepository, PasswordUpgraderInterface { - public function upgradePassword(UserInterface $user, string $newEncodedPassword): void - { - } + abstract public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/TestRepositoryFactory.php b/src/Symfony/Bridge/Doctrine/Tests/TestRepositoryFactory.php new file mode 100644 index 0000000000000..7b739a988a4e2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/TestRepositoryFactory.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Repository\RepositoryFactory; +use Doctrine\Persistence\ObjectRepository; + +/** + * @author Andreas Braun + */ +final class TestRepositoryFactory implements RepositoryFactory +{ + /** + * @var array + */ + private array $repositoryList = []; + + /** + * {@inheritdoc} + */ + public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository + { + $repositoryHash = $this->getRepositoryHash($entityManager, $entityName); + + return $this->repositoryList[$repositoryHash] ??= $this->createRepository($entityManager, $entityName); + } + + public function setRepository(EntityManagerInterface $entityManager, string $entityName, ObjectRepository $repository): void + { + $repositoryHash = $this->getRepositoryHash($entityManager, $entityName); + + $this->repositoryList[$repositoryHash] = $repository; + } + + private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository + { + $metadata = $entityManager->getClassMetadata($entityName); + $repositoryClassName = $metadata->customRepositoryClassName ?: $entityManager->getConfiguration()->getDefaultRepositoryClassName(); + + return new $repositoryClassName($entityManager, $metadata); + } + + private function getRepositoryHash(EntityManagerInterface $entityManager, string $entityName): string + { + return $entityManager->getClassMetadata($entityName)->getName().spl_object_hash($entityManager); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php new file mode 100644 index 0000000000000..fde2341bc9ebe --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Types\UlidType; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\Ulid; + +final class UlidTypeTest extends TestCase +{ + private const DUMMY_ULID = '01EEDQEK6ZAZE93J8KG5B4MBJC'; + + /** @var AbstractPlatform */ + private $platform; + + /** @var UlidType */ + private $type; + + public static function setUpBeforeClass(): void + { + if (Type::hasType('ulid')) { + Type::overrideType('ulid', UlidType::class); + } else { + Type::addType('ulid', UlidType::class); + } + } + + protected function setUp(): void + { + $this->platform = $this->createMock(AbstractPlatform::class); + $this->platform + ->method('hasNativeGuidType') + ->willReturn(true); + $this->platform + ->method('getGuidTypeDeclarationSQL') + ->willReturn('DUMMYVARCHAR()'); + + $this->type = Type::getType('ulid'); + } + + public function testUlidConvertsToDatabaseValue() + { + $ulid = Ulid::fromString(self::DUMMY_ULID); + + $expected = $ulid->toRfc4122(); + $actual = $this->type->convertToDatabaseValue($ulid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testUlidInterfaceConvertsToDatabaseValue() + { + $ulid = $this->createMock(AbstractUid::class); + + $ulid + ->expects($this->once()) + ->method('toRfc4122') + ->willReturn('foo'); + + $actual = $this->type->convertToDatabaseValue($ulid, $this->platform); + + $this->assertEquals('foo', $actual); + } + + public function testUlidStringConvertsToDatabaseValue() + { + $actual = $this->type->convertToDatabaseValue(self::DUMMY_ULID, $this->platform); + $ulid = Ulid::fromString(self::DUMMY_ULID); + + $expected = $ulid->toRfc4122(); + + $this->assertEquals($expected, $actual); + } + + public function testNotSupportedTypeConversionForDatabaseValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToDatabaseValue(new \stdClass(), $this->platform); + } + + public function testNullConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testUlidInterfaceConvertsToPHPValue() + { + $ulid = $this->createMock(AbstractUid::class); + $actual = $this->type->convertToPHPValue($ulid, $this->platform); + + $this->assertSame($ulid, $actual); + } + + public function testUlidConvertsToPHPValue() + { + $ulid = $this->type->convertToPHPValue(self::DUMMY_ULID, $this->platform); + + $this->assertInstanceOf(Ulid::class, $ulid); + $this->assertEquals(self::DUMMY_ULID, $ulid->__toString()); + } + + public function testInvalidUlidConversionForPHPValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToPHPValue('abcdefg', $this->platform); + } + + public function testNullConversionForPHPValue() + { + $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); + } + + public function testReturnValueIfUlidForPHPValue() + { + $ulid = new Ulid(); + + $this->assertSame($ulid, $this->type->convertToPHPValue($ulid, $this->platform)); + } + + public function testGetName() + { + $this->assertEquals('ulid', $this->type->getName()); + } + + public function testGetGuidTypeDeclarationSQL() + { + $this->assertEquals('DUMMYVARCHAR()', $this->type->getSqlDeclaration(['length' => 36], $this->platform)); + } + + public function testRequiresSQLCommentHint() + { + $this->assertTrue($this->type->requiresSQLCommentHint($this->platform)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php new file mode 100644 index 0000000000000..d6bf714627a1d --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Uid\AbstractUid; +use Symfony\Component\Uid\Uuid; + +final class UuidTypeTest extends TestCase +{ + private const DUMMY_UUID = '9f755235-5a2d-4aba-9605-e9962b312e50'; + + /** @var AbstractPlatform */ + private $platform; + + /** @var UuidType */ + private $type; + + public static function setUpBeforeClass(): void + { + if (Type::hasType('uuid')) { + Type::overrideType('uuid', UuidType::class); + } else { + Type::addType('uuid', UuidType::class); + } + } + + protected function setUp(): void + { + $this->platform = $this->createMock(AbstractPlatform::class); + $this->platform + ->method('hasNativeGuidType') + ->willReturn(true); + $this->platform + ->method('getGuidTypeDeclarationSQL') + ->willReturn('DUMMYVARCHAR()'); + + $this->type = Type::getType('uuid'); + } + + public function testUuidConvertsToDatabaseValue() + { + $uuid = Uuid::fromString(self::DUMMY_UUID); + + $expected = $uuid->__toString(); + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals($expected, $actual); + } + + public function testUuidInterfaceConvertsToDatabaseValue() + { + $uuid = $this->createMock(AbstractUid::class); + + $uuid + ->expects($this->once()) + ->method('toRfc4122') + ->willReturn('foo'); + + $actual = $this->type->convertToDatabaseValue($uuid, $this->platform); + + $this->assertEquals('foo', $actual); + } + + public function testUuidStringConvertsToDatabaseValue() + { + $actual = $this->type->convertToDatabaseValue(self::DUMMY_UUID, $this->platform); + + $this->assertEquals(self::DUMMY_UUID, $actual); + } + + public function testNotSupportedTypeConversionForDatabaseValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToDatabaseValue(new \stdClass(), $this->platform); + } + + public function testNullConversionForDatabaseValue() + { + $this->assertNull($this->type->convertToDatabaseValue(null, $this->platform)); + } + + public function testUuidInterfaceConvertsToPHPValue() + { + $uuid = $this->createMock(AbstractUid::class); + $actual = $this->type->convertToPHPValue($uuid, $this->platform); + + $this->assertSame($uuid, $actual); + } + + public function testUuidConvertsToPHPValue() + { + $uuid = $this->type->convertToPHPValue(self::DUMMY_UUID, $this->platform); + + $this->assertInstanceOf(Uuid::class, $uuid); + $this->assertEquals(self::DUMMY_UUID, $uuid->__toString()); + } + + public function testInvalidUuidConversionForPHPValue() + { + $this->expectException(ConversionException::class); + + $this->type->convertToPHPValue('abcdefg', $this->platform); + } + + public function testNullConversionForPHPValue() + { + $this->assertNull($this->type->convertToPHPValue(null, $this->platform)); + } + + public function testReturnValueIfUuidForPHPValue() + { + $uuid = Uuid::v4(); + + $this->assertSame($uuid, $this->type->convertToPHPValue($uuid, $this->platform)); + } + + public function testGetName() + { + $this->assertEquals('uuid', $this->type->getName()); + } + + public function testGetGuidTypeDeclarationSQL() + { + $this->assertEquals('DUMMYVARCHAR()', $this->type->getSqlDeclaration(['length' => 36], $this->platform)); + } + + public function testRequiresSQLCommentHint() + { + $this->assertTrue($this->type->requiresSQLCommentHint($this->platform)); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php new file mode 100644 index 0000000000000..9e334e8ff1dbb --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Validator\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; + +class UniqueEntityTest extends TestCase +{ + public function testAttributeWithDefaultProperty() + { + $metadata = new ClassMetadata(UniqueEntityDummyOne::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + /** @var UniqueEntity $constraint */ + [$constraint] = $metadata->getConstraints(); + self::assertSame(['email'], $constraint->fields); + self::assertTrue($constraint->ignoreNull); + self::assertSame('doctrine.orm.validator.unique', $constraint->validatedBy()); + self::assertSame(['Default', 'UniqueEntityDummyOne'], $constraint->groups); + } + + public function testAttributeWithCustomizedService() + { + $metadata = new ClassMetadata(UniqueEntityDummyTwo::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + /** @var UniqueEntity $constraint */ + [$constraint] = $metadata->getConstraints(); + self::assertSame(['isbn'], $constraint->fields); + self::assertSame('my_own_validator', $constraint->validatedBy()); + self::assertSame('my_own_entity_manager', $constraint->em); + self::assertSame('App\Entity\MyEntity', $constraint->entityClass); + self::assertSame('fetchDifferently', $constraint->repositoryMethod); + } + + public function testAttributeWithGroupsAndPaylod() + { + $metadata = new ClassMetadata(UniqueEntityDummyThree::class); + $loader = new AnnotationLoader(); + self::assertTrue($loader->loadClassMetadata($metadata)); + + /** @var UniqueEntity $constraint */ + [$constraint] = $metadata->getConstraints(); + self::assertSame('uuid', $constraint->fields); + self::assertSame('id', $constraint->errorPath); + self::assertSame('some attached data', $constraint->payload); + self::assertSame(['some_group'], $constraint->groups); + } +} + +#[UniqueEntity(['email'], message: 'myMessage')] +class UniqueEntityDummyOne +{ + private $email; +} + +#[UniqueEntity(fields: ['isbn'], service: 'my_own_validator', em: 'my_own_entity_manager', entityClass: 'App\Entity\MyEntity', repositoryMethod: 'fetchDifferently')] +class UniqueEntityDummyTwo +{ + private $isbn; +} + +#[UniqueEntity('uuid', ignoreNull: false, errorPath: 'id', payload: 'some attached data', groups: ['some_group'])] +class UniqueEntityDummyThree +{ + private $id; + private $uuid; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 3032e41f132ea..240e2eb976194 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -18,8 +18,7 @@ use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\ObjectRepository; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; -use Symfony\Bridge\Doctrine\Test\TestRepositoryFactory; +use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; @@ -34,6 +33,7 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapper; use Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapperType; +use Symfony\Bridge\Doctrine\Tests\TestRepositoryFactory; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -156,15 +156,11 @@ private function createSchema($em) /** * This is a functional test as there is a large integration necessary to get the validator working. + * + * @dataProvider provideUniquenessConstraints */ - public function testValidateUniqueness() + public function testValidateUniqueness(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name'], - 'em' => self::EM_NAME, - ]); - $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Foo'); @@ -190,15 +186,22 @@ public function testValidateUniqueness() ->assertRaised(); } - public function testValidateCustomErrorPath() + public function provideUniquenessConstraints(): iterable { - $constraint = new UniqueEntity([ + yield 'Doctrine style' => [new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], 'em' => self::EM_NAME, - 'errorPath' => 'bar', - ]); + ])]; + + yield 'Named arguments' => [new UniqueEntity(message: 'myMessage', fields: ['name'], em: 'foo')]; + } + /** + * @dataProvider provideConstraintsWithCustomErrorPath + */ + public function testValidateCustomErrorPath(UniqueEntity $constraint) + { $entity1 = new SingleIntIdEntity(1, 'Foo'); $entity2 = new SingleIntIdEntity(2, 'Foo'); @@ -216,14 +219,23 @@ public function testValidateCustomErrorPath() ->assertRaised(); } - public function testValidateUniquenessWithNull() + public function provideConstraintsWithCustomErrorPath(): iterable { - $constraint = new UniqueEntity([ + yield 'Doctrine style' => [new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], 'em' => self::EM_NAME, - ]); + 'errorPath' => 'bar', + ])]; + + yield 'Named arguments' => [new UniqueEntity(message: 'myMessage', fields: ['name'], em: 'foo', errorPath: 'bar')]; + } + /** + * @dataProvider provideUniquenessConstraints + */ + public function testValidateUniquenessWithNull(UniqueEntity $constraint) + { $entity1 = new SingleIntIdEntity(1, null); $entity2 = new SingleIntIdEntity(2, null); @@ -236,15 +248,11 @@ public function testValidateUniquenessWithNull() $this->assertNoViolation(); } - public function testValidateUniquenessWithIgnoreNullDisabled() + /** + * @dataProvider provideConstraintsWithIgnoreNullDisabled + */ + public function testValidateUniquenessWithIgnoreNullDisabled(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name', 'name2'], - 'em' => self::EM_NAME, - 'ignoreNull' => false, - ]); - $entity1 = new DoubleNameEntity(1, 'Foo', null); $entity2 = new DoubleNameEntity(2, 'Foo', null); @@ -270,30 +278,34 @@ public function testValidateUniquenessWithIgnoreNullDisabled() ->assertRaised(); } - public function testAllConfiguredFieldsAreCheckedOfBeingMappedByDoctrineWithIgnoreNullEnabled() + public function provideConstraintsWithIgnoreNullDisabled(): iterable { - $this->expectException(ConstraintDefinitionException::class); - $constraint = new UniqueEntity([ + yield 'Doctrine style' => [new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name', 'name2'], 'em' => self::EM_NAME, - 'ignoreNull' => true, - ]); + 'ignoreNull' => false, + ])]; + + yield 'Named arguments' => [new UniqueEntity(message: 'myMessage', fields: ['name', 'name2'], em: 'foo', ignoreNull: false)]; + } + /** + * @dataProvider provideConstraintsWithIgnoreNullEnabled + */ + public function testAllConfiguredFieldsAreCheckedOfBeingMappedByDoctrineWithIgnoreNullEnabled(UniqueEntity $constraint) + { $entity1 = new SingleIntIdEntity(1, null); + $this->expectException(\Symfony\Component\Validator\Exception\ConstraintDefinitionException::class); $this->validator->validate($entity1, $constraint); } - public function testNoValidationIfFirstFieldIsNullAndNullValuesAreIgnored() + /** + * @dataProvider provideConstraintsWithIgnoreNullEnabled + */ + public function testNoValidationIfFirstFieldIsNullAndNullValuesAreIgnored(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name', 'name2'], - 'em' => self::EM_NAME, - 'ignoreNull' => true, - ]); - $entity1 = new DoubleNullableNameEntity(1, null, 'Foo'); $entity2 = new DoubleNullableNameEntity(2, null, 'Foo'); @@ -313,6 +325,18 @@ public function testNoValidationIfFirstFieldIsNullAndNullValuesAreIgnored() $this->assertNoViolation(); } + public function provideConstraintsWithIgnoreNullEnabled(): iterable + { + yield 'Doctrine style' => [new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name', 'name2'], + 'em' => self::EM_NAME, + 'ignoreNull' => true, + ])]; + + yield 'Named arguments' => [new UniqueEntity(message: 'myMessage', fields: ['name', 'name2'], em: 'foo', ignoreNull: true)]; + } + public function testValidateUniquenessWithValidCustomErrorPath() { $constraint = new UniqueEntity([ @@ -347,15 +371,11 @@ public function testValidateUniquenessWithValidCustomErrorPath() ->assertRaised(); } - public function testValidateUniquenessUsingCustomRepositoryMethod() + /** + * @dataProvider provideConstraintsWithCustomRepositoryMethod + */ + public function testValidateUniquenessUsingCustomRepositoryMethod(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name'], - 'em' => self::EM_NAME, - 'repositoryMethod' => 'findByCustom', - ]); - $repository = $this->createRepositoryMock(); $repository->expects($this->once()) ->method('findByCustom') @@ -373,15 +393,11 @@ public function testValidateUniquenessUsingCustomRepositoryMethod() $this->assertNoViolation(); } - public function testValidateUniquenessWithUnrewoundArray() + /** + * @dataProvider provideConstraintsWithCustomRepositoryMethod + */ + public function testValidateUniquenessWithUnrewoundArray(UniqueEntity $constraint) { - $constraint = new UniqueEntity([ - 'message' => 'myMessage', - 'fields' => ['name'], - 'em' => self::EM_NAME, - 'repositoryMethod' => 'findByCustom', - ]); - $entity = new SingleIntIdEntity(1, 'foo'); $repository = $this->createRepositoryMock(); @@ -408,6 +424,18 @@ function () use ($entity) { $this->assertNoViolation(); } + public function provideConstraintsWithCustomRepositoryMethod(): iterable + { + yield 'Doctrine style' => [new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + 'repositoryMethod' => 'findByCustom', + ])]; + + yield 'Named arguments' => [new UniqueEntity(message: 'myMessage', fields: ['name'], em: 'foo', repositoryMethod: 'findByCustom')]; + } + /** * @dataProvider resultTypesProvider */ @@ -854,11 +882,7 @@ public function resultWithEmptyIterator(): array return [ [$entity, new class() implements \Iterator { - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - public function current() + public function current(): mixed { return null; } @@ -872,11 +896,7 @@ public function next(): void { } - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - public function key() + public function key(): mixed { return false; } @@ -886,11 +906,7 @@ public function rewind(): void } }], [$entity, new class() implements \Iterator { - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - public function current() + public function current(): mixed { return false; } @@ -904,11 +920,7 @@ public function next(): void { } - /** - * @return mixed - */ - #[\ReturnTypeWillChange] - public function key() + public function key(): mixed { return false; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php index 0bdc2efc2a77a..034cd001aaebb 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php @@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping\Column; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\BaseUser; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity; @@ -48,8 +48,8 @@ protected function setUp(): void public function testLoadClassMetadata() { $validator = Validation::createValidatorBuilder() - ->addMethodMapping('loadValidatorMetadata') - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}')) ->getValidator() ; @@ -162,7 +162,8 @@ public function testExtractEnum() $validator = Validation::createValidatorBuilder() ->addMethodMapping('loadValidatorMetadata') - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}')) ->getValidator() ; @@ -179,8 +180,8 @@ public function testExtractEnum() public function testFieldMappingsConfiguration() { $validator = Validation::createValidatorBuilder() - ->addMethodMapping('loadValidatorMetadata') - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->addXmlMappings([__DIR__.'/../Resources/validator/BaseUser.xml']) ->addLoader( new DoctrineLoader( @@ -221,7 +222,8 @@ public function regexpProvider() public function testClassNoAutoMapping() { $validator = Validation::createValidatorBuilder() - ->enableAnnotationMapping() + ->enableAnnotationMapping(true) + ->addDefaultDoctrineAnnotationReader() ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{.*}')) ->getValidator(); diff --git a/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php b/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php new file mode 100644 index 0000000000000..54f6d01ee4ea2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/AbstractUidType.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Types\ConversionException; +use Doctrine\DBAL\Types\Type; +use Symfony\Component\Uid\AbstractUid; + +abstract class AbstractUidType extends Type +{ + abstract protected function getUidClass(): string; + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $column, AbstractPlatform $platform): string + { + if ($platform->hasNativeGuidType()) { + return $platform->getGuidTypeDeclarationSQL($column); + } + + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => '16', + 'fixed' => true, + ]); + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?AbstractUid + { + if ($value instanceof AbstractUid || null === $value) { + return $value; + } + + if (!\is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + return $this->getUidClass()::fromString($value); + } catch (\InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + /** + * {@inheritdoc} + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string + { + $toString = $platform->hasNativeGuidType() ? 'toRfc4122' : 'toBinary'; + + if ($value instanceof AbstractUid) { + return $value->$toString(); + } + + if (null === $value || '' === $value) { + return null; + } + + if (!\is_string($value)) { + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'string', AbstractUid::class]); + } + + try { + return $this->getUidClass()::fromString($value)->$toString(); + } catch (\InvalidArgumentException $e) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + } + + /** + * {@inheritdoc} + */ + public function requiresSQLCommentHint(AbstractPlatform $platform): bool + { + return true; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/UlidType.php b/src/Symfony/Bridge/Doctrine/Types/UlidType.php new file mode 100644 index 0000000000000..809317b222005 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/UlidType.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Symfony\Component\Uid\Ulid; + +final class UlidType extends AbstractUidType +{ + public function getName(): string + { + return 'ulid'; + } + + protected function getUidClass(): string + { + return Ulid::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Types/UuidType.php b/src/Symfony/Bridge/Doctrine/Types/UuidType.php new file mode 100644 index 0000000000000..bbf0394034a06 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Types/UuidType.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Types; + +use Symfony\Component\Uid\Uuid; + +final class UuidType extends AbstractUidType +{ + public function getName(): string + { + return 'uuid'; + } + + protected function getUidClass(): string + { + return Uuid::class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index 3839f14f35330..ebfd5e62046d3 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -21,6 +21,7 @@ * * @author Benjamin Eberlei */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] class UniqueEntity extends Constraint { public const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f'; @@ -38,17 +39,50 @@ class UniqueEntity extends Constraint self::NOT_UNIQUE_ERROR => 'NOT_UNIQUE_ERROR', ]; - public function getRequiredOptions() + /** + * {@inheritdoc} + * + * @param array|string $fields the combination of fields that must contain unique values or a set of options + */ + public function __construct( + $fields, + string $message = null, + string $service = null, + string $em = null, + string $entityClass = null, + string $repositoryMethod = null, + string $errorPath = null, + bool $ignoreNull = null, + array $groups = null, + $payload = null, + array $options = [] + ) { + if (\is_array($fields) && \is_string(key($fields))) { + $options = array_merge($fields, $options); + } elseif (null !== $fields) { + $options['fields'] = $fields; + } + + parent::__construct($options, $groups, $payload); + + $this->message = $message ?? $this->message; + $this->service = $service ?? $this->service; + $this->em = $em ?? $this->em; + $this->entityClass = $entityClass ?? $this->entityClass; + $this->repositoryMethod = $repositoryMethod ?? $this->repositoryMethod; + $this->errorPath = $errorPath ?? $this->errorPath; + $this->ignoreNull = $ignoreNull ?? $this->ignoreNull; + } + + public function getRequiredOptions(): array { return ['fields']; } /** * The validator must be defined as a service with this name. - * - * @return string */ - public function validatedBy() + public function validatedBy(): string { return $this->service; } @@ -56,12 +90,12 @@ public function validatedBy() /** * {@inheritdoc} */ - public function getTargets() + public function getTargets(): string|array { return self::CLASS_CONSTRAINT; } - public function getDefaultOption() + public function getDefaultOption(): ?string { return 'fields'; } diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index cce8cb9079723..e56ff0f026175 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -12,6 +12,8 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -38,7 +40,7 @@ public function __construct(ManagerRegistry $registry) * @throws UnexpectedTypeException * @throws ConstraintDefinitionException */ - public function validate($entity, Constraint $constraint) + public function validate(mixed $entity, Constraint $constraint) { if (!$constraint instanceof UniqueEntity) { throw new UnexpectedTypeException($constraint, UniqueEntity::class); @@ -76,7 +78,7 @@ public function validate($entity, Constraint $constraint) $em = $this->registry->getManagerForClass(\get_class($entity)); if (!$em) { - throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', \get_class($entity))); + throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', get_debug_type($entity))); } } @@ -137,7 +139,18 @@ public function validate($entity, Constraint $constraint) $repository = $em->getRepository(\get_class($entity)); } - $result = $repository->{$constraint->repositoryMethod}($criteria); + $arguments = [$criteria]; + + /* If the default repository method is used, it is always enough to retrieve at most two entities because: + * - No entity returned, the current entity is definitely unique. + * - More than one entity returned, the current entity cannot be unique. + * - One entity returned the uniqueness depends on the current entity. + */ + if ('findBy' === $constraint->repositoryMethod) { + $arguments = [$criteria, null, 2]; + } + + $result = $repository->{$constraint->repositoryMethod}(...$arguments); if ($result instanceof \IteratorAggregate) { $result = $result->getIterator(); @@ -180,13 +193,13 @@ public function validate($entity, Constraint $constraint) ->addViolation(); } - private function formatWithIdentifiers($em, $class, $value) + private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, mixed $value) { if (!\is_object($value) || $value instanceof \DateTimeInterface) { return $this->formatValue($value, self::PRETTY_DATE); } - if (method_exists($value, '__toString')) { + if ($value instanceof \Stringable) { return (string) $value; } diff --git a/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php b/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php index 659bd8569759d..28d5fccc7459c 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php +++ b/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php @@ -28,7 +28,7 @@ public function __construct(ManagerRegistry $registry) $this->registry = $registry; } - public function initialize($object) + public function initialize(object $object) { $manager = $this->registry->getManagerForClass(\get_class($object)); if (null !== $manager) { diff --git a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php index 7ea316f41a2d0..d1918fc6de45b 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php +++ b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php @@ -34,7 +34,7 @@ final class DoctrineLoader implements LoaderInterface use AutoMappingTrait; private $entityManager; - private $classValidatorRegexp; + private ?string $classValidatorRegexp; public function __construct(EntityManagerInterface $entityManager, string $classValidatorRegexp = null) { diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index cce2609cf1a89..d482efec66af8 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -16,48 +16,53 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=8.0.2", "doctrine/event-manager": "~1.0", - "doctrine/persistence": "^1.3|^2", + "doctrine/persistence": "^2", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2" + "symfony/service-contracts": "^1.1|^2|^3" }, "require-dev": { - "composer/package-versions-deprecated": "^1.8", - "symfony/stopwatch": "^3.4|^4.0|^5.0", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/form": "^4.4.11|^5.0.11", - "symfony/http-kernel": "^4.3.7", - "symfony/messenger": "^4.4|^5.0", - "symfony/property-access": "^3.4|^4.0|^5.0", - "symfony/property-info": "^3.4|^4.0|^5.0", - "symfony/proxy-manager-bridge": "^3.4|^4.0|^5.0", - "symfony/security-core": "^4.4|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/validator": "^4.4.2|^5.0.2", - "symfony/var-dumper": "^3.4|^4.0|^5.0", - "symfony/translation": "^3.4|^4.0|^5.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/doctrine-messenger": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/proxy-manager-bridge": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/uid": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0", "doctrine/annotations": "^1.10.4", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", - "doctrine/dbal": "^2.7|^3.0", - "doctrine/orm": "^2.6.3" + "doctrine/dbal": "^2.13.1|^3.0", + "doctrine/orm": "^2.7.4", + "psr/log": "^1|^2|^3" }, "conflict": { - "doctrine/dbal": "<2.7", - "doctrine/orm": "<2.6.3", + "doctrine/dbal": "<2.13.1", "doctrine/lexer": "<1.1", - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", - "symfony/dependency-injection": "<3.4", - "symfony/form": "<4.4", - "symfony/http-kernel": "<4.3.7", - "symfony/messenger": "<4.3", - "symfony/proxy-manager-bridge": "<4.4.19", - "symfony/security-core": "<4.4", - "symfony/validator": "<4.4.2|<5.0.2,>=5.0" + "doctrine/orm": "<2.7.4", + "phpunit/phpunit": "<5.4.3", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/form": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<5.4", + "symfony/property-info": "<5.4", + "symfony/security-bundle": "<5.4", + "symfony/security-core": "<6.0", + "symfony/validator": "<5.4" }, "suggest": { "symfony/form": "", diff --git a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist index fa76fa9b500e7..34c19c68c319d 100644 --- a/src/Symfony/Bridge/Doctrine/phpunit.xml.dist +++ b/src/Symfony/Bridge/Doctrine/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 61ab0b3c6f899..14c0e5882d015 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,42 @@ CHANGELOG ========= +6.0 +--- + + * The `$actionLevel` constructor argument of `NotFoundActivationStrategy` has been replaced by the `$inner` one which expects an `ActivationStrategyInterface` to decorate instead + * The `$actionLevel` constructor argument of `HttpCodeActivationStrategy` has been replaced by the `$inner` one which expects an `ActivationStrategyInterface` to decorate instead + * Remove `ResetLoggersWorkerSubscriber` in favor of "reset_on_message" option in messenger configuration + * Remove `SwiftMailerHandler`, use `MailerHandler` instead + +5.4 +--- + + * Deprecate `ResetLoggersWorkerSubscriber` to reset buffered logs in messenger + workers, use "reset_on_message" option in messenger configuration instead. + +5.3 +--- + + * Add `ResetLoggersWorkerSubscriber` to reset buffered logs in messenger workers + +5.2.0 +----- + + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` has been deprecated and replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy` will become final in 6.0. + * The `$actionLevel` constructor argument of `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` has been deprecated and replaced by the `$inner` one which expects an ActivationStrategyInterface to decorate instead. `Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy` will become final in 6.0 + +5.1.0 +----- + + * Added `MailerHandler` + +5.0.0 +----- + + * The methods `DebugProcessor::getLogs()`, `DebugProcessor::countErrors()`, `Logger::getLogs()` and `Logger::countErrors()` have a new `$request` argument. + * Added support for Monolog 2. + 4.4.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php index 0e5fddc222875..5a5dd1774a066 100644 --- a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php +++ b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php @@ -15,6 +15,7 @@ use Monolog\Logger; use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\RuntimeException; @@ -26,6 +27,7 @@ /** * @author Grégoire Pineau */ +#[AsCommand(name: 'server:log', description: 'Start a log server that displays logs in real time')] class ServerLogCommand extends Command { private const BG_COLOR = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']; @@ -33,9 +35,7 @@ class ServerLogCommand extends Command private $el; private $handler; - protected static $defaultName = 'server:log'; - - public function isEnabled() + public function isEnabled(): bool { if (!class_exists(ConsoleFormatter::class)) { return false; @@ -60,7 +60,6 @@ protected function configure() ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT) ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE) ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"') - ->setDescription('Start a log server that displays logs in real time') ->setHelp(<<<'EOF' %command.name% starts a log server to display in real time the log messages generated by your application: @@ -75,7 +74,7 @@ protected function configure() ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $filter = $input->getOption('filter'); if ($filter) { diff --git a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php index 4b87c264e4d5a..b7d2adc7dffa6 100644 --- a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php +++ b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php @@ -41,9 +41,14 @@ class ConsoleFormatter implements FormatterInterface Logger::EMERGENCY => 'fg=white;bg=red', ]; - private $options; + private array $options; private $cloner; + + /** + * @var resource|null + */ private $outputBuffer; + private $dumper; /** @@ -83,10 +88,8 @@ public function __construct(array $options = []) /** * {@inheritdoc} - * - * @return mixed */ - public function formatBatch(array $records) + public function formatBatch(array $records): mixed { foreach ($records as $key => $record) { $records[$key] = $this->format($record); @@ -97,10 +100,8 @@ public function formatBatch(array $records) /** * {@inheritdoc} - * - * @return mixed */ - public function format(array $record) + public function format(array $record): mixed { $record = $this->replacePlaceHolder($record); @@ -145,7 +146,7 @@ public function echoLine(string $line, int $depth, string $indentPad) /** * @internal */ - public function castObject($v, array $a, Stub $s, bool $isNested): array + public function castObject(mixed $v, array $a, Stub $s, bool $isNested): array { if ($this->options['multiline']) { return $a; @@ -182,9 +183,9 @@ private function replacePlaceHolder(array $record): array return $record; } - private function dumpData($data, bool $colors = null): string + private function dumpData(mixed $data, bool $colors = null): string { - if (null === $this->dumper) { + if (!isset($this->dumper)) { return ''; } diff --git a/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php index 54988766c3a2d..d895cac1d5c6b 100644 --- a/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php +++ b/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php @@ -28,10 +28,8 @@ public function __construct(VarCloner $cloner = null) /** * {@inheritdoc} - * - * @return mixed */ - public function format(array $record) + public function format(array $record): mixed { $record['context'] = $this->cloner->cloneVar($record['context']); $record['extra'] = $this->cloner->cloneVar($record['extra']); @@ -41,10 +39,8 @@ public function format(array $record) /** * {@inheritdoc} - * - * @return mixed */ - public function formatBatch(array $records) + public function formatBatch(array $records): mixed { foreach ($records as $k => $record) { $record[$k] = $this->format($record); diff --git a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php index 4d722c46ecfcf..4a90ade3f8a2c 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php @@ -13,30 +13,26 @@ use Monolog\Handler\ChromePHPHandler as BaseChromePhpHandler; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; /** * ChromePhpHandler. * * @author Christophe Coevoet * - * @final since Symfony 4.3 + * @final */ class ChromePhpHandler extends BaseChromePhpHandler { - private $headers = []; - - /** - * @var Response - */ + private array $headers = []; private $response; /** * Adds the headers to the response once it's created. */ - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -57,13 +53,13 @@ public function onKernelResponse(FilterResponseEvent $event) /** * {@inheritdoc} */ - protected function sendHeader($header, $content) + protected function sendHeader($header, $content): void { if (!self::$sendHeaders) { return; } - if ($this->response) { + if (isset($this->response)) { $this->response->headers->set($header, $content); } else { $this->headers[$header] = $content; @@ -72,10 +68,8 @@ protected function sendHeader($header, $content) /** * Override default behavior since we check it in onKernelResponse. - * - * @return bool */ - protected function headersAccepted() + protected function headersAccepted(): bool { return true; } diff --git a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php index 2a02905d24e21..3c42efd8f510a 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php @@ -44,14 +44,14 @@ class ConsoleHandler extends AbstractProcessingHandler implements EventSubscriberInterface { private $output; - private $verbosityLevelMap = [ + private array $verbosityLevelMap = [ OutputInterface::VERBOSITY_QUIET => Logger::ERROR, OutputInterface::VERBOSITY_NORMAL => Logger::WARNING, OutputInterface::VERBOSITY_VERBOSE => Logger::NOTICE, OutputInterface::VERBOSITY_VERY_VERBOSE => Logger::INFO, OutputInterface::VERBOSITY_DEBUG => Logger::DEBUG, ]; - private $consoleFormatterOptions; + private array $consoleFormatterOptions; /** * @param OutputInterface|null $output The console output to use (the handler remains disabled when passing null @@ -74,20 +74,16 @@ public function __construct(OutputInterface $output = null, bool $bubble = true, /** * {@inheritdoc} - * - * @return bool */ - public function isHandling(array $record) + public function isHandling(array $record): bool { return $this->updateLevel() && parent::isHandling($record); } /** * {@inheritdoc} - * - * @return bool */ - public function handle(array $record) + public function handle(array $record): bool { // we have to update the logging level each time because the verbosity of the // console output might have changed in the meantime (it is not immutable) @@ -105,7 +101,7 @@ public function setOutput(OutputInterface $output) /** * Disables the output. */ - public function close() + public function close(): void { $this->output = null; @@ -137,7 +133,7 @@ public function onTerminate(ConsoleTerminateEvent $event) /** * {@inheritdoc} */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ ConsoleEvents::COMMAND => ['onCommand', 255], @@ -147,10 +143,8 @@ public static function getSubscribedEvents() /** * {@inheritdoc} - * - * @return void */ - protected function write(array $record) + protected function write(array $record): void { // at this point we've determined for sure that we want to output the record, so use the output's own verbosity $this->output->write((string) $record['formatted'], false, $this->output->getVerbosity()); @@ -158,10 +152,8 @@ protected function write(array $record) /** * {@inheritdoc} - * - * @return FormatterInterface */ - protected function getDefaultFormatter() + protected function getDefaultFormatter(): FormatterInterface { if (!class_exists(CliDumper::class)) { return new LineFormatter(); diff --git a/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php b/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php index a59825f6ab1f4..66dade361c641 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * Push logs directly to Elasticsearch and format them according to Logstash specification. @@ -44,15 +45,16 @@ class ElasticsearchLogstashHandler extends AbstractHandler use FormattableHandlerTrait; use ProcessableHandlerTrait; - private $endpoint; - private $index; + private string $endpoint; + private string $index; private $client; - private $responses; /** - * @param string|int $level The minimum logging level at which this handler will be triggered + * @var \SplObjectStorage */ - public function __construct(string $endpoint = 'http://127.0.0.1:9200', string $index = 'monolog', HttpClientInterface $client = null, $level = Logger::DEBUG, bool $bubble = true) + private \SplObjectStorage $responses; + + public function __construct(string $endpoint = 'http://127.0.0.1:9200', string $index = 'monolog', HttpClientInterface $client = null, string|int $level = Logger::DEBUG, bool $bubble = true) { if (!interface_exists(HttpClientInterface::class)) { throw new \LogicException(sprintf('The "%s" handler needs an HTTP client. Try running "composer require symfony/http-client".', __CLASS__)); @@ -129,10 +131,7 @@ private function sendToElasticsearch(array $records) $this->wait(false); } - /** - * @return array - */ - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php index 84f61ce9bf706..ddc5443b1ab18 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php @@ -11,25 +11,25 @@ namespace Symfony\Bridge\Monolog\Handler\FingersCrossed; -use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; -use Symfony\Component\HttpFoundation\RequestStack; +use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Symfony\Component\HttpKernel\Exception\HttpException; /** * Activation strategy that ignores certain HTTP codes. * * @author Shaun Simmons + * @author Pierrick Vignand */ -class HttpCodeActivationStrategy extends ErrorLevelActivationStrategy +final class HttpCodeActivationStrategy implements ActivationStrategyInterface { - private $exclusions; - private $requestStack; - /** * @param array $exclusions each exclusion must have a "code" and "urls" keys */ - public function __construct(RequestStack $requestStack, array $exclusions, $actionLevel) - { + public function __construct( + private $requestStack, + private array $exclusions, + private $inner, + ) { foreach ($exclusions as $exclusion) { if (!\array_key_exists('code', $exclusion)) { throw new \LogicException('An exclusion must have a "code" key.'); @@ -38,25 +38,17 @@ public function __construct(RequestStack $requestStack, array $exclusions, $acti throw new \LogicException('An exclusion must have a "urls" key.'); } } - - parent::__construct($actionLevel); - - $this->requestStack = $requestStack; - $this->exclusions = $exclusions; } - /** - * @return bool - */ - public function isHandlerActivated(array $record) + public function isHandlerActivated(array $record): bool { - $isActivated = parent::isHandlerActivated($record); + $isActivated = $this->inner->isHandlerActivated($record); if ( $isActivated && isset($record['context']['exception']) && $record['context']['exception'] instanceof HttpException - && ($request = $this->requestStack->getMasterRequest()) + && ($request = $this->requestStack->getMainRequest()) ) { foreach ($this->exclusions as $exclusion) { if ($record['context']['exception']->getStatusCode() !== $exclusion['code']) { diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php index a404f39db3cab..22f54bdf6d2da 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php @@ -11,8 +11,7 @@ namespace Symfony\Bridge\Monolog\Handler\FingersCrossed; -use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; -use Symfony\Component\HttpFoundation\RequestStack; +use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Symfony\Component\HttpKernel\Exception\HttpException; /** @@ -20,33 +19,30 @@ * * @author Jordi Boggiano * @author Fabien Potencier + * @author Pierrick Vignand */ -class NotFoundActivationStrategy extends ErrorLevelActivationStrategy +final class NotFoundActivationStrategy implements ActivationStrategyInterface { - private $exclude; - private $requestStack; + private string $exclude; - public function __construct(RequestStack $requestStack, array $excludedUrls, $actionLevel) - { - parent::__construct($actionLevel); - - $this->requestStack = $requestStack; + public function __construct( + private $requestStack, + array $excludedUrls, + private $inner + ) { $this->exclude = '{('.implode('|', $excludedUrls).')}i'; } - /** - * @return bool - */ - public function isHandlerActivated(array $record) + public function isHandlerActivated(array $record): bool { - $isActivated = parent::isHandlerActivated($record); + $isActivated = $this->inner->isHandlerActivated($record); if ( $isActivated && isset($record['context']['exception']) && $record['context']['exception'] instanceof HttpException && 404 == $record['context']['exception']->getStatusCode() - && ($request = $this->requestStack->getMasterRequest()) + && ($request = $this->requestStack->getMainRequest()) ) { return !preg_match($this->exclude, $request->getPathInfo()); } diff --git a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php index f006118223cba..c9dc672c98021 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php @@ -13,30 +13,26 @@ use Monolog\Handler\FirePHPHandler as BaseFirePHPHandler; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; /** * FirePHPHandler. * * @author Jordi Boggiano * - * @final since Symfony 4.3 + * @final */ class FirePHPHandler extends BaseFirePHPHandler { - private $headers = []; - - /** - * @var Response - */ + private array $headers = []; private $response; /** * Adds the headers to the response once it's created. */ - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -59,7 +55,7 @@ public function onKernelResponse(FilterResponseEvent $event) /** * {@inheritdoc} */ - protected function sendHeader($header, $content) + protected function sendHeader($header, $content): void { if (!self::$sendHeaders) { return; @@ -74,10 +70,8 @@ protected function sendHeader($header, $content) /** * Override default behavior since we check the user agent in onKernelResponse. - * - * @return bool */ - protected function headersAccepted() + protected function headersAccepted(): bool { return true; } diff --git a/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php new file mode 100644 index 0000000000000..07208979bc451 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\HtmlFormatter; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +/** + * @author Alexander Borisov + */ +class MailerHandler extends AbstractProcessingHandler +{ + private $mailer; + private $messageTemplate; + + public function __construct(MailerInterface $mailer, callable|Email $messageTemplate, string|int $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->messageTemplate = !\is_callable($messageTemplate) || $messageTemplate instanceof \Closure ? $messageTemplate : \Closure::fromCallable($messageTemplate); + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + $messages[] = $this->processRecord($record); + } + + if (!empty($messages)) { + $this->send((string) $this->getFormatter()->formatBatch($messages), $messages); + } + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->send((string) $record['formatted'], [$record]); + } + + /** + * Send a mail with the given content. + * + * @param string $content formatted email body to be sent + * @param array $records the array of log records that formed this content + */ + protected function send(string $content, array $records) + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Message subject. + * + * @param string $format The format of the subject + */ + protected function getSubjectFormatter(string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Message to be sent. + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + */ + protected function buildMessage(string $content, array $records): Email + { + $message = null; + if ($this->messageTemplate instanceof Email) { + $message = clone $this->messageTemplate; + } elseif (\is_callable($this->messageTemplate)) { + $message = ($this->messageTemplate)($content, $records); + if (!$message instanceof Email) { + throw new \InvalidArgumentException(sprintf('Could not resolve message from a callable. Instance of "%s" is expected.', Email::class)); + } + } else { + throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it.'); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->subject($subjectFormatter->format($this->getHighestRecord($records))); + } + + if ($this->getFormatter() instanceof HtmlFormatter) { + if ($message->getHtmlCharset()) { + $message->html($content, $message->getHtmlCharset()); + } else { + $message->html($content); + } + } else { + if ($message->getTextCharset()) { + $message->text($content, $message->getTextCharset()); + } else { + $message->text($content); + } + } + + return $message; + } + + protected function getHighestRecord(array $records): array + { + $highestRecord = null; + foreach ($records as $record) { + if (null === $highestRecord || $highestRecord['level'] < $record['level']) { + $highestRecord = $record; + } + } + + return $highestRecord; + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/NotifierHandler.php b/src/Symfony/Bridge/Monolog/Handler/NotifierHandler.php new file mode 100644 index 0000000000000..13961b1bec890 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/NotifierHandler.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Handler; + +use Monolog\Handler\AbstractHandler; +use Monolog\Logger; +use Symfony\Component\Notifier\Notification\Notification; +use Symfony\Component\Notifier\Notifier; +use Symfony\Component\Notifier\NotifierInterface; + +/** + * Uses Notifier as a log handler. + * + * @author Fabien Potencier + */ +class NotifierHandler extends AbstractHandler +{ + private $notifier; + + public function __construct(NotifierInterface $notifier, string|int $level = Logger::ERROR, bool $bubble = true) + { + $this->notifier = $notifier; + + parent::__construct(Logger::toMonologLevel($level) < Logger::ERROR ? Logger::ERROR : $level, $bubble); + } + + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + $this->notify([$record]); + + return !$this->bubble; + } + + public function handleBatch(array $records): void + { + if ($records = array_filter($records, [$this, 'isHandling'])) { + $this->notify($records); + } + } + + private function notify(array $records): void + { + $record = $this->getHighestRecord($records); + if (($record['context']['exception'] ?? null) instanceof \Throwable) { + $notification = Notification::fromThrowable($record['context']['exception']); + } else { + $notification = new Notification($record['message']); + } + + $notification->importanceFromLogLevelName(Logger::getLevelName($record['level'])); + + $this->notifier->send($notification, ...$this->notifier->getAdminRecipients()); + } + + private function getHighestRecord(array $records) + { + $highestRecord = null; + foreach ($records as $record) { + if (null === $highestRecord || $highestRecord['level'] < $record['level']) { + $highestRecord = $record; + } + } + + return $highestRecord; + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php index 8101178d66c03..b14d8e241cf13 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php @@ -12,23 +12,57 @@ namespace Symfony\Bridge\Monolog\Handler; use Monolog\Formatter\FormatterInterface; -use Monolog\Handler\AbstractHandler; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Handler\FormattableHandlerTrait; use Monolog\Logger; use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter; +if (trait_exists(FormattableHandlerTrait::class)) { + class ServerLogHandler extends AbstractProcessingHandler + { + use ServerLogHandlerTrait; + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new VarDumperFormatter(); + } + } +} else { + class ServerLogHandler extends AbstractProcessingHandler + { + use ServerLogHandlerTrait; + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter() + { + return new VarDumperFormatter(); + } + } +} + /** * @author Grégoire Pineau */ -class ServerLogHandler extends AbstractHandler +trait ServerLogHandlerTrait { - private $host; + private string $host; + + /** + * @var resource + */ private $context; - private $socket; /** - * @param string|int $level The minimum logging level at which this handler will be triggered + * @var resource|null */ - public function __construct(string $host, $level = Logger::DEBUG, bool $bubble = true, array $context = []) + private $socket; + + public function __construct(string $host, string|int $level = Logger::DEBUG, bool $bubble = true, array $context = []) { parent::__construct($level, $bubble); @@ -42,10 +76,8 @@ public function __construct(string $host, $level = Logger::DEBUG, bool $bubble = /** * {@inheritdoc} - * - * @return bool */ - public function handle(array $record) + public function handle(array $record): bool { if (!$this->isHandling($record)) { return false; @@ -61,6 +93,11 @@ public function handle(array $record) restore_error_handler(); } + return parent::handle($record); + } + + protected function write(array $record): void + { $recordFormatted = $this->formatRecord($record); set_error_handler(self::class.'::nullErrorHandler'); @@ -77,16 +114,12 @@ public function handle(array $record) } finally { restore_error_handler(); } - - return false === $this->bubble; } /** * {@inheritdoc} - * - * @return FormatterInterface */ - protected function getDefaultFormatter() + protected function getDefaultFormatter(): FormatterInterface { return new VarDumperFormatter(); } @@ -108,13 +141,7 @@ private function createSocket() private function formatRecord(array $record): string { - if ($this->processors) { - foreach ($this->processors as $processor) { - $record = $processor($record); - } - } - - $recordFormatted = $this->getFormatter()->format($record); + $recordFormatted = $record['formatted']; foreach (['log_uuid', 'uuid', 'uid'] as $key) { if (isset($record['extra'][$key])) { diff --git a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php deleted file mode 100644 index 1143c0668093c..0000000000000 --- a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Monolog\Handler; - -use Monolog\Handler\SwiftMailerHandler as BaseSwiftMailerHandler; -use Symfony\Component\Console\Event\ConsoleTerminateEvent; -use Symfony\Component\HttpKernel\Event\PostResponseEvent; - -/** - * Extended SwiftMailerHandler that flushes mail queue if necessary. - * - * @author Philipp Kräutli - * - * @final since Symfony 4.3 - */ -class SwiftMailerHandler extends BaseSwiftMailerHandler -{ - protected $transport; - - protected $instantFlush = false; - - public function setTransport(\Swift_Transport $transport) - { - $this->transport = $transport; - } - - /** - * After the kernel has been terminated we will always flush messages. - */ - public function onKernelTerminate(PostResponseEvent $event) - { - $this->instantFlush = true; - } - - /** - * After the CLI application has been terminated we will always flush messages. - */ - public function onCliTerminate(ConsoleTerminateEvent $event) - { - $this->instantFlush = true; - } - - /** - * {@inheritdoc} - */ - protected function send($content, array $records) - { - parent::send($content, $records); - - if ($this->instantFlush) { - $this->flushMemorySpool(); - } - } - - /** - * {@inheritdoc} - */ - public function reset() - { - $this->flushMemorySpool(); - } - - /** - * Flushes the mail queue if a memory spool is used. - */ - private function flushMemorySpool() - { - $mailerTransport = $this->mailer->getTransport(); - if (!$mailerTransport instanceof \Swift_Transport_SpoolTransport) { - return; - } - - $spool = $mailerTransport->getSpool(); - if (!$spool instanceof \Swift_MemorySpool) { - return; - } - - if (null === $this->transport) { - throw new \Exception('No transport available to flush mail queue.'); - } - - $spool->flushQueue($this->transport); - } -} diff --git a/src/Symfony/Bridge/Monolog/Logger.php b/src/Symfony/Bridge/Monolog/Logger.php index 336d2e7f0dc33..2b5f312053048 100644 --- a/src/Symfony/Bridge/Monolog/Logger.php +++ b/src/Symfony/Bridge/Monolog/Logger.php @@ -24,17 +24,11 @@ class Logger extends BaseLogger implements DebugLoggerInterface, ResetInterface { /** * {@inheritdoc} - * - * @param Request|null $request */ - public function getLogs(/* Request $request = null */) + public function getLogs(Request $request = null): array { - if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); - } - if ($logger = $this->getDebugLogger()) { - return $logger->getLogs(...\func_get_args()); + return $logger->getLogs($request); } return []; @@ -42,17 +36,11 @@ public function getLogs(/* Request $request = null */) /** * {@inheritdoc} - * - * @param Request|null $request */ - public function countErrors(/* Request $request = null */) + public function countErrors(Request $request = null): int { - if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); - } - if ($logger = $this->getDebugLogger()) { - return $logger->countErrors(...\func_get_args()); + return $logger->countErrors($request); } return 0; @@ -71,7 +59,7 @@ public function clear() /** * {@inheritdoc} */ - public function reset() + public function reset(): void { $this->clear(); diff --git a/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php new file mode 100644 index 0000000000000..f98969700bcab --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Processor; + +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * The base class for security token processors. + * + * @author Dany Maillard + * @author Igor Timoshenko + */ +abstract class AbstractTokenProcessor +{ + /** + * @var TokenStorageInterface + */ + protected $tokenStorage; + + public function __construct(TokenStorageInterface $tokenStorage) + { + $this->tokenStorage = $tokenStorage; + } + + abstract protected function getKey(): string; + + abstract protected function getToken(): ?TokenInterface; + + public function __invoke(array $record): array + { + $record['extra'][$this->getKey()] = null; + + if (null !== $token = $this->getToken()) { + $record['extra'][$this->getKey()] = [ + 'authenticated' => method_exists($token, 'isAuthenticated') ? $token->isAuthenticated(false) : (bool) $token->getUser(), + 'roles' => $token->getRoleNames(), + ]; + + $record['extra'][$this->getKey()]['user_identifier'] = $token->getUserIdentifier(); + } + + return $record; + } +} diff --git a/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php b/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php index 2891f4f2f4916..a1e1c144379ba 100644 --- a/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/ConsoleCommandProcessor.php @@ -23,9 +23,9 @@ */ class ConsoleCommandProcessor implements EventSubscriberInterface, ResetInterface { - private $commandData; - private $includeArguments; - private $includeOptions; + private array $commandData; + private bool $includeArguments; + private bool $includeOptions; public function __construct(bool $includeArguments = true, bool $includeOptions = false) { @@ -35,7 +35,7 @@ public function __construct(bool $includeArguments = true, bool $includeOptions public function __invoke(array $records) { - if (null !== $this->commandData && !isset($records['extra']['command'])) { + if (isset($this->commandData) && !isset($records['extra']['command'])) { $records['extra']['command'] = $this->commandData; } @@ -44,7 +44,7 @@ public function __invoke(array $records) public function reset() { - $this->commandData = null; + unset($this->commandData); } public function addCommandData(ConsoleEvent $event) @@ -60,7 +60,7 @@ public function addCommandData(ConsoleEvent $event) } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ ConsoleEvents::COMMAND => ['addCommandData', 1], diff --git a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php index 11f075fe3a26e..0f2ae6cda0d74 100644 --- a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php @@ -19,8 +19,8 @@ class DebugProcessor implements DebugLoggerInterface, ResetInterface { - private $records = []; - private $errorCount = []; + private array $records = []; + private array $errorCount = []; private $requestStack; public function __construct(RequestStack $requestStack = null) @@ -32,8 +32,17 @@ public function __invoke(array $record) { $hash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; + $timestamp = $timestampRfc3339 = false; + if ($record['datetime'] instanceof \DateTimeInterface) { + $timestamp = $record['datetime']->getTimestamp(); + $timestampRfc3339 = $record['datetime']->format(\DateTimeInterface::RFC3339_EXTENDED); + } elseif (false !== $timestamp = strtotime($record['datetime'])) { + $timestampRfc3339 = (new \DateTimeImmutable($record['datetime']))->format(\DateTimeInterface::RFC3339_EXTENDED); + } + $this->records[$hash][] = [ - 'timestamp' => $record['datetime'] instanceof \DateTimeInterface ? $record['datetime']->getTimestamp() : strtotime($record['datetime']), + 'timestamp' => $timestamp, + 'timestamp_rfc3339' => $timestampRfc3339, 'message' => $record['message'], 'priority' => $record['level'], 'priorityName' => $record['level_name'], @@ -58,16 +67,10 @@ public function __invoke(array $record) /** * {@inheritdoc} - * - * @param Request|null $request */ - public function getLogs(/* Request $request = null */) + public function getLogs(Request $request = null): array { - if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); - } - - if (1 <= \func_num_args() && null !== $request = func_get_arg(0)) { + if (null !== $request) { return $this->records[spl_object_hash($request)] ?? []; } @@ -80,16 +83,10 @@ public function getLogs(/* Request $request = null */) /** * {@inheritdoc} - * - * @param Request|null $request */ - public function countErrors(/* Request $request = null */) + public function countErrors(Request $request = null): int { - if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); - } - - if (1 <= \func_num_args() && null !== $request = func_get_arg(0)) { + if (null !== $request) { return $this->errorCount[spl_object_hash($request)] ?? 0; } diff --git a/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php b/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php index 09507b55e7fb2..0bb738f378532 100644 --- a/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/RouteProcessor.php @@ -13,7 +13,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\Service\ResetInterface; @@ -22,12 +22,12 @@ * * @author Piotr Stankowski * - * @final since Symfony 4.4 + * @final */ class RouteProcessor implements EventSubscriberInterface, ResetInterface { - private $routeData; - private $includeParams; + private array $routeData = []; + private bool $includeParams; public function __construct(bool $includeParams = true) { @@ -35,7 +35,7 @@ public function __construct(bool $includeParams = true) $this->reset(); } - public function __invoke(array $records) + public function __invoke(array $records): array { if ($this->routeData && !isset($records['extra']['requests'])) { $records['extra']['requests'] = array_values($this->routeData); @@ -49,9 +49,9 @@ public function reset() $this->routeData = []; } - public function addRouteData(GetResponseEvent $event) + public function addRouteData(RequestEvent $event) { - if ($event->isMasterRequest()) { + if ($event->isMainRequest()) { $this->reset(); } @@ -78,7 +78,7 @@ public function removeRouteData(FinishRequestEvent $event) unset($this->routeData[$requestId]); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => ['addRouteData', 1], diff --git a/src/Symfony/Bridge/Monolog/Processor/SwitchUserTokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/SwitchUserTokenProcessor.php new file mode 100644 index 0000000000000..76aa7e479d0e5 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Processor/SwitchUserTokenProcessor.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Processor; + +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Adds the original security token to the log entry. + * + * @author Igor Timoshenko + */ +class SwitchUserTokenProcessor extends AbstractTokenProcessor +{ + /** + * {@inheritdoc} + */ + protected function getKey(): string + { + return 'impersonator_token'; + } + + /** + * {@inheritdoc} + */ + protected function getToken(): ?TokenInterface + { + $token = $this->tokenStorage->getToken(); + + if ($token instanceof SwitchUserToken) { + return $token->getOriginalToken(); + } + + return null; + } +} diff --git a/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php index 7613d01361962..7ca212eb29770 100644 --- a/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php @@ -11,39 +11,29 @@ namespace Symfony\Bridge\Monolog\Processor; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; /** * Adds the current security token to the log entry. * * @author Dany Maillard + * @author Igor Timoshenko */ -class TokenProcessor +class TokenProcessor extends AbstractTokenProcessor { - private $tokenStorage; - - public function __construct(TokenStorageInterface $tokenStorage) + /** + * {@inheritdoc} + */ + protected function getKey(): string { - $this->tokenStorage = $tokenStorage; + return 'token'; } - public function __invoke(array $records) + /** + * {@inheritdoc} + */ + protected function getToken(): ?TokenInterface { - $records['extra']['token'] = null; - if (null !== $token = $this->tokenStorage->getToken()) { - if (method_exists($token, 'getRoleNames')) { - $roles = $token->getRoleNames(); - } else { - $roles = array_map(function ($role) { return $role->getRole(); }, $token->getRoles(false)); - } - - $records['extra']['token'] = [ - 'username' => $token->getUsername(), - 'authenticated' => $token->isAuthenticated(), - 'roles' => $roles, - ]; - } - - return $records; + return $this->tokenStorage->getToken(); } } diff --git a/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php b/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php index 71bf71a816327..f72023cdfdac4 100644 --- a/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/WebProcessor.php @@ -13,7 +13,7 @@ use Monolog\Processor\WebProcessor as BaseWebProcessor; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; /** @@ -21,7 +21,7 @@ * * @author Jordi Boggiano * - * @final since Symfony 4.3 + * @final */ class WebProcessor extends BaseWebProcessor implements EventSubscriberInterface { @@ -31,15 +31,15 @@ public function __construct(array $extraFields = null) parent::__construct([], $extraFields); } - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { - if ($event->isMasterRequest()) { + if ($event->isMainRequest()) { $this->serverData = $event->getRequest()->server->all(); $this->serverData['REMOTE_ADDR'] = $event->getRequest()->getClientIp(); } } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => ['onKernelRequest', 4096], diff --git a/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.php b/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.php index 31c62e3e75591..e258c7942a20a 100644 --- a/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.php +++ b/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.php @@ -12,16 +12,17 @@ namespace Symfony\Bridge\Monolog\Tests; use Symfony\Bridge\Monolog\Logger; +use Symfony\Component\HttpFoundation\Request; class ClassThatInheritLogger extends Logger { - public function getLogs(): array + public function getLogs(Request $request = null): array { - return parent::getLogs(); + return parent::getLogs($request); } - public function countErrors(): int + public function countErrors(Request $request = null): int { - return parent::countErrors(); + return parent::countErrors($request); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php index 3d84cb3552c0d..ea6931670d863 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Tests\Handler\FingersCrossed; +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Monolog\Handler\FingersCrossed\HttpCodeActivationStrategy; @@ -23,13 +24,13 @@ class HttpCodeActivationStrategyTest extends TestCase public function testExclusionsWithoutCode() { $this->expectException(\LogicException::class); - new HttpCodeActivationStrategy(new RequestStack(), [['urls' => []]], Logger::WARNING); + new HttpCodeActivationStrategy(new RequestStack(), [['urls' => []]], new ErrorLevelActivationStrategy(Logger::WARNING)); } public function testExclusionsWithoutUrls() { $this->expectException(\LogicException::class); - new HttpCodeActivationStrategy(new RequestStack(), [['code' => 404]], Logger::WARNING); + new HttpCodeActivationStrategy(new RequestStack(), [['code' => 404]], new ErrorLevelActivationStrategy(Logger::WARNING)); } /** @@ -48,13 +49,13 @@ public function testIsActivated($url, $record, $expected) ['code' => 405, 'urls' => []], ['code' => 400, 'urls' => ['^/400/a', '^/400/b']], ], - Logger::WARNING + new ErrorLevelActivationStrategy(Logger::WARNING) ); - $this->assertEquals($expected, $strategy->isHandlerActivated($record)); + self::assertEquals($expected, $strategy->isHandlerActivated($record)); } - public function isActivatedProvider() + public function isActivatedProvider(): array { return [ ['/test', ['level' => Logger::ERROR], true], @@ -70,7 +71,7 @@ public function isActivatedProvider() ]; } - protected function getContextException($code) + private function getContextException(int $code): array { return ['exception' => new HttpException($code)]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php index b04678106c96e..95590186d55f3 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/NotFoundActivationStrategyTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Tests\Handler\FingersCrossed; +use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy; @@ -23,17 +24,17 @@ class NotFoundActivationStrategyTest extends TestCase /** * @dataProvider isActivatedProvider */ - public function testIsActivated($url, $record, $expected) + public function testIsActivated(string $url, array $record, bool $expected) { $requestStack = new RequestStack(); $requestStack->push(Request::create($url)); - $strategy = new NotFoundActivationStrategy($requestStack, ['^/foo', 'bar'], Logger::WARNING); + $strategy = new NotFoundActivationStrategy($requestStack, ['^/foo', 'bar'], new ErrorLevelActivationStrategy(Logger::WARNING)); - $this->assertEquals($expected, $strategy->isHandlerActivated($record)); + self::assertEquals($expected, $strategy->isHandlerActivated($record)); } - public function isActivatedProvider() + public function isActivatedProvider(): array { return [ ['/test', ['level' => Logger::DEBUG], false], @@ -48,7 +49,7 @@ public function isActivatedProvider() ]; } - protected function getContextException($code) + protected function getContextException(int $code): array { return ['exception' => new HttpException($code)]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php new file mode 100644 index 0000000000000..daec7676c9e99 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Handler; + +use Monolog\Formatter\HtmlFormatter; +use Monolog\Formatter\LineFormatter; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Handler\MailerHandler; +use Symfony\Bridge\Monolog\Logger; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +class MailerHandlerTest extends TestCase +{ + /** @var MockObject|MailerInterface */ + private $mailer = null; + + protected function setUp(): void + { + $this->mailer = $this->createMock(MailerInterface::class); + } + + public function testHandle() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new LineFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: WARNING message' === $email->getSubject() && null === $email->getHtmlBody(); + })) + ; + $handler->handle($this->getRecord(Logger::WARNING, 'message')); + } + + public function testHandleBatch() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new LineFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: ERROR error' === $email->getSubject() && null === $email->getHtmlBody(); + })) + ; + $handler->handleBatch($this->getMultipleRecords()); + } + + public function testMessageCreationIsLazyWhenUsingCallback() + { + $this->mailer + ->expects($this->never()) + ->method('send') + ; + + $callback = function () { + throw new \RuntimeException('Email creation callback should not have been called in this test'); + }; + $handler = new MailerHandler($this->mailer, $callback, Logger::ALERT); + + $records = [ + $this->getRecord(Logger::DEBUG), + $this->getRecord(Logger::INFO), + ]; + $handler->handleBatch($records); + } + + public function testHtmlContent() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new HtmlFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: WARNING message' === $email->getSubject() && null === $email->getTextBody(); + })) + ; + $handler->handle($this->getRecord(Logger::WARNING, 'message')); + } + + protected function getRecord($level = Logger::WARNING, $message = 'test', $context = []): array + { + return [ + 'message' => $message, + 'context' => $context, + 'level' => $level, + 'level_name' => Logger::getLevelName($level), + 'channel' => 'test', + 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))), + 'extra' => [], + ]; + } + + protected function getMultipleRecords(): array + { + return [ + $this->getRecord(Logger::DEBUG, 'debug message 1'), + $this->getRecord(Logger::DEBUG, 'debug message 2'), + $this->getRecord(Logger::INFO, 'information'), + $this->getRecord(Logger::WARNING, 'warning'), + $this->getRecord(Logger::ERROR, 'error'), + ]; + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php b/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php index 12b687e115cdb..e862d780e7eb9 100644 --- a/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php @@ -125,15 +125,15 @@ public function testReset() } } - /** - * @group legacy - * @expectedDeprecation The "Symfony\Bridge\Monolog\Logger::getLogs()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. - * @expectedDeprecation The "Symfony\Bridge\Monolog\Logger::countErrors()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. - */ - public function testInheritedClassWithoutArgument() + public function testInheritedClassCallGetLogsWithoutArgument() { $loggerChild = new ClassThatInheritLogger('test'); - $loggerChild->getLogs(); - $loggerChild->countErrors(); + $this->assertSame([], $loggerChild->getLogs()); + } + + public function testInheritedClassCallCountErrorsWithoutArgument() + { + $loggerChild = new ClassThatInheritLogger('test'); + $this->assertEquals(0, $loggerChild->countErrors()); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.php b/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.php index 1f15bd9f764b2..bc87c724c9d31 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.php @@ -12,16 +12,17 @@ namespace Symfony\Bridge\Monolog\Tests\Processor; use Symfony\Bridge\Monolog\Processor\DebugProcessor; +use Symfony\Component\HttpFoundation\Request; class ClassThatInheritDebugProcessor extends DebugProcessor { - public function getLogs(): array + public function getLogs(Request $request = null): array { - return parent::getLogs(); + return parent::getLogs($request); } - public function countErrors(): int + public function countErrors(Request $request = null): int { - return parent::countErrors(); + return parent::countErrors($request); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php index 4ac41c978ec4e..c576462d0abfe 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php @@ -43,6 +43,30 @@ public function providerDatetimeFormatTests(): array ]; } + /** + * @dataProvider providerDatetimeRfc3339FormatTests + */ + public function testDatetimeRfc3339Format(array $record, $expectedTimestamp) + { + $processor = new DebugProcessor(); + $processor($record); + + $records = $processor->getLogs(); + self::assertCount(1, $records); + self::assertSame($expectedTimestamp, $records[0]['timestamp_rfc3339']); + } + + public function providerDatetimeRfc3339FormatTests(): array + { + $record = $this->getRecord(); + + return [ + [array_merge($record, ['datetime' => new \DateTime('2019-01-01T00:01:00+00:00')]), '2019-01-01T00:01:00.000+00:00'], + [array_merge($record, ['datetime' => '2019-01-01T00:01:00+00:00']), '2019-01-01T00:01:00.000+00:00'], + [array_merge($record, ['datetime' => 'foo']), false], + ]; + } + public function testDebugProcessor() { $processor = new DebugProcessor(); @@ -87,16 +111,16 @@ public function testWithRequestStack() $this->assertSame(0, $processor->countErrors(new Request())); } - /** - * @group legacy - * @expectedDeprecation The "Symfony\Bridge\Monolog\Processor\DebugProcessor::getLogs()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. - * @expectedDeprecation The "Symfony\Bridge\Monolog\Processor\DebugProcessor::countErrors()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. - */ - public function testInheritedClassWithoutArgument() + public function testInheritedClassCallGetLogsWithoutArgument() + { + $debugProcessorChild = new ClassThatInheritDebugProcessor(); + $this->assertSame([], $debugProcessorChild->getLogs()); + } + + public function testInheritedClassCallCountErrorsWithoutArgument() { $debugProcessorChild = new ClassThatInheritDebugProcessor(); - $debugProcessorChild->getLogs(); - $debugProcessorChild->countErrors(); + $this->assertEquals(0, $debugProcessorChild->countErrors()); } private function getRecord($level = Logger::WARNING, $message = 'test'): array diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php index 06336f1a593ca..6e6afa92c4409 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php @@ -123,14 +123,14 @@ public function testProcessorDoesNothingWhenNoRequest() $this->assertEquals(['extra' => []], $record); } - private function getRequestEvent(Request $request, int $requestType = HttpKernelInterface::MASTER_REQUEST): RequestEvent + private function getRequestEvent(Request $request, int $requestType = HttpKernelInterface::MAIN_REQUEST): RequestEvent { return new RequestEvent($this->createMock(HttpKernelInterface::class), $request, $requestType); } private function getFinishRequestEvent(Request $request): FinishRequestEvent { - return new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); + return new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); } private function mockEmptyRequest(): Request diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php new file mode 100644 index 0000000000000..602e9db61a82d --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/SwitchUserTokenProcessorTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Processor; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Processor\SwitchUserTokenProcessor; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\InMemoryUser; + +/** + * Tests the SwitchUserTokenProcessor. + * + * @author Igor Timoshenko + */ +class SwitchUserTokenProcessorTest extends TestCase +{ + public function testProcessor() + { + $originalToken = new UsernamePasswordToken(new InMemoryUser('original_user', 'password', ['ROLE_SUPER_ADMIN']), 'provider', ['ROLE_SUPER_ADMIN']); + $switchUserToken = new SwitchUserToken(new InMemoryUser('user', 'passsword', ['ROLE_USER']), 'provider', ['ROLE_USER'], $originalToken); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->method('getToken')->willReturn($switchUserToken); + + $processor = new SwitchUserTokenProcessor($tokenStorage); + $record = ['extra' => []]; + $record = $processor($record); + + $expected = [ + 'impersonator_token' => [ + 'authenticated' => true, + 'roles' => ['ROLE_SUPER_ADMIN'], + 'user_identifier' => 'original_user', + ], + ]; + + $this->assertEquals($expected, $record['extra']); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php index dcaf0f647e301..c9e37cfdb2c45 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Monolog\Processor\TokenProcessor; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\InMemoryUser; /** * Tests the TokenProcessor. @@ -25,7 +26,11 @@ class TokenProcessorTest extends TestCase { public function testProcessor() { - $token = new UsernamePasswordToken('user', 'password', 'provider', ['ROLE_USER']); + if (!method_exists(UsernamePasswordToken::class, 'getUserIdentifier')) { + $this->markTestSkipped('This test requires symfony/security-core 5.3+'); + } + + $token = new UsernamePasswordToken(new InMemoryUser('user', 'password', ['ROLE_USER']), 'provider', ['ROLE_USER']); $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->method('getToken')->willReturn($token); @@ -34,8 +39,7 @@ public function testProcessor() $record = $processor($record); $this->assertArrayHasKey('token', $record['extra']); - $this->assertEquals($token->getUsername(), $record['extra']['token']['username']); - $this->assertEquals($token->isAuthenticated(), $record['extra']['token']['authenticated']); + $this->assertEquals($token->getUserIdentifier(), $record['extra']['token']['user_identifier']); $this->assertEquals(['ROLE_USER'], $record['extra']['token']['roles']); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php index 73cb964d9ab6c..9b70b4bbfbc25 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php @@ -38,7 +38,7 @@ public function testUsesRequestServerData() public function testUseRequestClientIp() { - Request::setTrustedProxies(['192.168.0.1'], Request::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['192.168.0.1'], Request::HEADER_X_FORWARDED_FOR); [$event, $server] = $this->createRequestEvent(['X_FORWARDED_FOR' => '192.168.0.2']); $processor = new WebProcessor(); @@ -89,7 +89,7 @@ private function createRequestEvent(array $additionalServerParameters = []): arr $request->server->replace($server); $request->headers->replace($server); - $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); return [$event, $server]; } diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 613b619b450ed..1fd9424f683a9 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -16,21 +16,24 @@ } ], "require": { - "php": ">=7.1.3", - "monolog/monolog": "^1.25.1", - "symfony/service-contracts": "^1.1|^2", - "symfony/http-kernel": "^4.3", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.0.2", + "monolog/monolog": "^1.25.1|^2", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/http-kernel": "^5.4|^6.0" }, "require-dev": { - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/security-core": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^3.4|^4.0|^5.0" + "symfony/console": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/var-dumper": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0" }, "conflict": { - "symfony/console": "<3.4", - "symfony/http-foundation": "<3.4" + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<6.0" }, "suggest": { "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", diff --git a/src/Symfony/Bridge/Monolog/phpunit.xml.dist b/src/Symfony/Bridge/Monolog/phpunit.xml.dist index 1bda3eca9cd05..ab47262381599 100644 --- a/src/Symfony/Bridge/Monolog/phpunit.xml.dist +++ b/src/Symfony/Bridge/Monolog/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md index e9fc8f43c6e86..bdcd3ea0d7819 100644 --- a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md +++ b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md @@ -1,6 +1,32 @@ CHANGELOG ========= +6.0 +--- + + * Remove `SetUpTearDownTrait` + +5.3 +--- + + * bumped the minimum PHP version to 7.1.3 + * bumped the minimum PHPUnit version to 7.5 + * deprecated the `SetUpTearDownTrait` trait, use original methods with "void" return typehint. + * added `logFile` option to write deprecations to a file instead of echoing them + +5.1.0 +----- + + * ignore verbosity settings when the build fails because of deprecations + * added per-group verbosity + * added `ExpectDeprecationTrait` to be able to define an expected deprecation from inside a test + * deprecated the `@expectedDeprecation` annotation, use the `ExpectDeprecationTrait::expectDeprecation()` method instead + +5.0.0 +----- + + * removed `weak_vendor` mode, use `max[self]=0` instead + 4.4.0 ----- diff --git a/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php b/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php index d61d7887be891..70fdb9f9631ad 100644 --- a/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php +++ b/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php @@ -51,7 +51,7 @@ public static function trait_exists($name, $autoload = true) public static function register($class) { - $self = \get_called_class(); + $self = static::class; $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; if (0 < strpos($class, '\\Tests\\')) { diff --git a/src/Symfony/Bridge/PhpUnit/ClockMock.php b/src/Symfony/Bridge/PhpUnit/ClockMock.php index 2cc834cd4f679..7280d44dc16f8 100644 --- a/src/Symfony/Bridge/PhpUnit/ClockMock.php +++ b/src/Symfony/Bridge/PhpUnit/ClockMock.php @@ -92,7 +92,7 @@ public static function gmdate($format, $timestamp = null) public static function register($class) { - $self = \get_called_class(); + $self = static::class; $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; if (0 < strpos($class, '\\Tests\\')) { diff --git a/src/Symfony/Bridge/PhpUnit/ConstraintTrait.php b/src/Symfony/Bridge/PhpUnit/ConstraintTrait.php index cecac95ab56d4..77f32e1b3753c 100644 --- a/src/Symfony/Bridge/PhpUnit/ConstraintTrait.php +++ b/src/Symfony/Bridge/PhpUnit/ConstraintTrait.php @@ -15,17 +15,12 @@ use ReflectionClass; $r = new ReflectionClass(Constraint::class); -if (\PHP_VERSION_ID < 70000 || !$r->getMethod('matches')->hasReturnType()) { - trait ConstraintTrait - { - use Legacy\ConstraintTraitForV6; - } -} elseif ($r->getProperty('exporter')->isProtected()) { +if ($r->getProperty('exporter')->isProtected()) { trait ConstraintTrait { use Legacy\ConstraintTraitForV7; } -} elseif (\PHP_VERSION_ID < 70100 || !$r->getMethod('evaluate')->hasReturnType()) { +} elseif (!$r->getMethod('evaluate')->hasReturnType()) { trait ConstraintTrait { use Legacy\ConstraintTraitForV8; diff --git a/src/Symfony/Bridge/PhpUnit/CoverageListener.php b/src/Symfony/Bridge/PhpUnit/CoverageListener.php index 805f9222a50d9..766252b8728b7 100644 --- a/src/Symfony/Bridge/PhpUnit/CoverageListener.php +++ b/src/Symfony/Bridge/PhpUnit/CoverageListener.php @@ -11,16 +11,109 @@ namespace Symfony\Bridge\PhpUnit; -if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { - class_alias('Symfony\Bridge\PhpUnit\Legacy\CoverageListenerForV5', 'Symfony\Bridge\PhpUnit\CoverageListener'); -} elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) { - class_alias('Symfony\Bridge\PhpUnit\Legacy\CoverageListenerForV6', 'Symfony\Bridge\PhpUnit\CoverageListener'); -} else { - class_alias('Symfony\Bridge\PhpUnit\Legacy\CoverageListenerForV7', 'Symfony\Bridge\PhpUnit\CoverageListener'); -} +use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestListener; +use PHPUnit\Framework\TestListenerDefaultImplementation; +use PHPUnit\Framework\Warning; +use PHPUnit\Util\Annotation\Registry; +use PHPUnit\Util\Test as TestUtil; + +class CoverageListener implements TestListener +{ + use TestListenerDefaultImplementation; + + private $sutFqcnResolver; + private $warningOnSutNotFound; + + public function __construct(callable $sutFqcnResolver = null, bool $warningOnSutNotFound = false) + { + $this->sutFqcnResolver = $sutFqcnResolver ?? static function (Test $test): ?string { + $class = \get_class($test); + + $sutFqcn = str_replace('\\Tests\\', '\\', $class); + $sutFqcn = preg_replace('{Test$}', '', $sutFqcn); + + return class_exists($sutFqcn) ? $sutFqcn : null; + }; + + $this->warningOnSutNotFound = $warningOnSutNotFound; + } + + public function startTest(Test $test): void + { + if (!$test instanceof TestCase) { + return; + } + + $annotations = TestUtil::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); + + $ignoredAnnotations = ['covers', 'coversDefaultClass', 'coversNothing']; + + foreach ($ignoredAnnotations as $annotation) { + if (isset($annotations['class'][$annotation]) || isset($annotations['method'][$annotation])) { + return; + } + } + + $sutFqcn = ($this->sutFqcnResolver)($test); + if (!$sutFqcn) { + if ($this->warningOnSutNotFound) { + $test->getTestResultObject()->addWarning($test, new Warning('Could not find the tested class.'), 0); + } -if (false) { - class CoverageListener + return; + } + + $covers = $sutFqcn; + if (!\is_array($sutFqcn)) { + $covers = [$sutFqcn]; + while ($parent = get_parent_class($sutFqcn)) { + $covers[] = $parent; + $sutFqcn = $parent; + } + } + + if (class_exists(Registry::class)) { + $this->addCoversForDocBlockInsideRegistry($test, $covers); + + return; + } + + $this->addCoversForClassToAnnotationCache($test, $covers); + } + + private function addCoversForClassToAnnotationCache(Test $test, array $covers): void { + $r = new \ReflectionProperty(TestUtil::class, 'annotationCache'); + $r->setAccessible(true); + + $cache = $r->getValue(); + $cache = array_replace_recursive($cache, [ + \get_class($test) => [ + 'covers' => $covers, + ], + ]); + + $r->setValue(TestUtil::class, $cache); + } + + private function addCoversForDocBlockInsideRegistry(Test $test, array $covers): void + { + $docBlock = Registry::getInstance()->forClassName(\get_class($test)); + + $symbolAnnotations = new \ReflectionProperty($docBlock, 'symbolAnnotations'); + $symbolAnnotations->setAccessible(true); + + // Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException + $covers = array_filter($covers, function (string $class) { + $reflector = new \ReflectionClass($class); + + return $reflector->isUserDefined(); + }); + + $symbolAnnotations->setValue($docBlock, array_replace($docBlock->symbolAnnotations(), [ + 'covers' => $covers, + ])); } } diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 00fb4bc8a2433..2aeae49d58fa9 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -16,6 +16,7 @@ use PHPUnit\Util\ErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; use Symfony\Component\ErrorHandler\DebugClassLoader; /** @@ -25,41 +26,33 @@ */ class DeprecationErrorHandler { - /** - * @deprecated since Symfony 4.3, use max[self]=0 instead - */ - const MODE_WEAK_VENDORS = 'weak_vendors'; - - const MODE_DISABLED = 'disabled'; - const MODE_WEAK = 'max[total]=999999&verbose=0'; - const MODE_STRICT = 'max[total]=0'; + public const MODE_DISABLED = 'disabled'; + public const MODE_WEAK = 'max[total]=999999&verbose=0'; + public const MODE_STRICT = 'max[total]=0'; private $mode; private $configuration; - private $deprecations = [ - 'unsilencedCount' => 0, - 'remaining selfCount' => 0, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 0, - 'remaining indirectCount' => 0, - 'unsilenced' => [], - 'remaining self' => [], - 'legacy' => [], - 'other' => [], - 'remaining direct' => [], - 'remaining indirect' => [], - ]; + + /** + * @var DeprecationGroup[] + */ + private $deprecationGroups = []; private static $isRegistered = false; private static $errorHandler; + public function __construct() + { + $this->resetDeprecationGroups(); + } + /** * Registers and configures the deprecation handler. * * The mode is a query string with options: - * - "disabled" to disable the deprecation handler + * - "disabled" to enable/disable the deprecation handler * - "verbose" to enable/disable displaying the deprecation report + * - "quiet" to disable displaying the deprecation report only for some groups (i.e. quiet[]=other) * - "max" to configure the number of deprecations to allow before exiting with a non-zero * status code; it's an array with keys "total", "self", "direct" and "indirect" * @@ -143,6 +136,9 @@ public function handleError($type, $msg, $file, $line, $context = []) if ($deprecation->isMuted()) { return null; } + if ($this->getConfiguration()->isBaselineDeprecation($deprecation)) { + return null; + } $msg = $deprecation->getMessage(); @@ -152,9 +148,9 @@ public function handleError($type, $msg, $file, $line, $context = []) $group = 'legacy'; } else { $group = [ - Deprecation::TYPE_SELF => 'remaining self', - Deprecation::TYPE_DIRECT => 'remaining direct', - Deprecation::TYPE_INDIRECT => 'remaining indirect', + Deprecation::TYPE_SELF => 'self', + Deprecation::TYPE_DIRECT => 'direct', + Deprecation::TYPE_INDIRECT => 'indirect', Deprecation::TYPE_UNDETERMINED => 'other', ][$deprecation->getType()]; } @@ -165,20 +161,16 @@ public function handleError($type, $msg, $file, $line, $context = []) exit(1); } - if ('legacy' !== $group) { - $ref = &$this->deprecations[$group][$msg]['count']; - ++$ref; - - if ($deprecation->originatesFromAnObject()) { - $class = $deprecation->originatingClass(); - $method = $deprecation->originatingMethod(); - $ref = &$this->deprecations[$group][$msg][$class.'::'.$method]; - ++$ref; - } + if ('legacy' === $group) { + $this->deprecationGroups[$group]->addNotice(); + } elseif ($deprecation->originatesFromAnObject()) { + $class = $deprecation->originatingClass(); + $method = $deprecation->originatingMethod(); + $this->deprecationGroups[$group]->addNoticeFromObject($msg, $class, $method); + } else { + $this->deprecationGroups[$group]->addNoticeFromProceduralCode($msg); } - ++$this->deprecations[$group.'Count']; - return null; } @@ -203,47 +195,55 @@ public function shutdown() echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n"; } - $groups = ['unsilenced', 'remaining self', 'remaining direct', 'remaining indirect', 'legacy', 'other']; - - $this->displayDeprecations($groups, $configuration); + $groups = array_keys($this->deprecationGroups); // store failing status - $isFailing = !$configuration->tolerates($this->deprecations); + $isFailing = !$configuration->tolerates($this->deprecationGroups); - // reset deprecations array - foreach ($this->deprecations as $group => $arrayOrInt) { - $this->deprecations[$group] = \is_int($arrayOrInt) ? 0 : []; - } + $this->displayDeprecations($groups, $configuration, $isFailing); + + $this->resetDeprecationGroups(); register_shutdown_function(function () use ($isFailing, $groups, $configuration) { - foreach ($this->deprecations as $group => $arrayOrInt) { - if (0 < (\is_int($arrayOrInt) ? $arrayOrInt : \count($arrayOrInt))) { + foreach ($this->deprecationGroups as $group) { + if ($group->count() > 0) { echo "Shutdown-time deprecations:\n"; break; } } - $this->displayDeprecations($groups, $configuration); + $isFailingAtShutdown = !$configuration->tolerates($this->deprecationGroups); + $this->displayDeprecations($groups, $configuration, $isFailingAtShutdown); + + if ($configuration->isGeneratingBaseline()) { + $configuration->writeBaseline(); + } - if ($isFailing || !$configuration->tolerates($this->deprecations)) { + if ($isFailing || $isFailingAtShutdown) { exit(1); } }); } + private function resetDeprecationGroups() + { + $this->deprecationGroups = [ + 'unsilenced' => new DeprecationGroup(), + 'self' => new DeprecationGroup(), + 'direct' => new DeprecationGroup(), + 'indirect' => new DeprecationGroup(), + 'legacy' => new DeprecationGroup(), + 'other' => new DeprecationGroup(), + ]; + } + private function getConfiguration() { if (null !== $this->configuration) { return $this->configuration; } if (false === $mode = $this->mode) { - if (isset($_SERVER['SYMFONY_DEPRECATIONS_HELPER'])) { - $mode = $_SERVER['SYMFONY_DEPRECATIONS_HELPER']; - } elseif (isset($_ENV['SYMFONY_DEPRECATIONS_HELPER'])) { - $mode = $_ENV['SYMFONY_DEPRECATIONS_HELPER']; - } else { - $mode = getenv('SYMFONY_DEPRECATIONS_HELPER'); - } + $mode = $_SERVER['SYMFONY_DEPRECATIONS_HELPER'] ?? $_ENV['SYMFONY_DEPRECATIONS_HELPER'] ?? getenv('SYMFONY_DEPRECATIONS_HELPER'); } if ('strict' === $mode) { return $this->configuration = Configuration::inStrictMode(); @@ -254,13 +254,6 @@ private function getConfiguration() if ('weak' === $mode) { return $this->configuration = Configuration::inWeakMode(); } - if (self::MODE_WEAK_VENDORS === $mode) { - ++$this->deprecations['remaining directCount']; - $msg = sprintf('Setting SYMFONY_DEPRECATIONS_HELPER to "%s" is deprecated in favor of "max[self]=0"', $mode); - $ref = &$this->deprecations['remaining direct'][$msg]['count']; - ++$ref; - $mode = 'max[self]=0'; - } if (isset($mode[0]) && '/' === $mode[0]) { return $this->configuration = Configuration::fromRegex($mode); } @@ -296,33 +289,52 @@ private static function colorize($str, $red) /** * @param string[] $groups * @param Configuration $configuration + * @param bool $isFailing + * + * @throws \InvalidArgumentException */ - private function displayDeprecations($groups, $configuration) + private function displayDeprecations($groups, $configuration, $isFailing) { $cmp = function ($a, $b) { - return $b['count'] - $a['count']; + return $b->count() - $a->count(); }; + if ($configuration->shouldWriteToLogFile()) { + if (false === $handle = @fopen($file = $configuration->getLogFile(), 'a')) { + throw new \InvalidArgumentException(sprintf('The configured log file "%s" is not writeable.', $file)); + } + } else { + $handle = fopen('php://output', 'w'); + } + foreach ($groups as $group) { - if ($this->deprecations[$group.'Count']) { - echo "\n", self::colorize( - sprintf('%s deprecation notices (%d)', ucfirst($group), $this->deprecations[$group.'Count']), - 'legacy' !== $group && 'remaining indirect' !== $group - ), "\n"; + if ($this->deprecationGroups[$group]->count()) { + $deprecationGroupMessage = sprintf( + '%s deprecation notices (%d)', + \in_array($group, ['direct', 'indirect', 'self'], true) ? "Remaining $group" : ucfirst($group), + $this->deprecationGroups[$group]->count() + ); + if ($configuration->shouldWriteToLogFile()) { + fwrite($handle, "\n$deprecationGroupMessage\n"); + } else { + fwrite($handle, "\n".self::colorize($deprecationGroupMessage, 'legacy' !== $group && 'indirect' !== $group)."\n"); + } - if (!$configuration->verboseOutput()) { + if ('legacy' !== $group && !$configuration->verboseOutput($group) && !$isFailing) { continue; } - uasort($this->deprecations[$group], $cmp); + $notices = $this->deprecationGroups[$group]->notices(); + uasort($notices, $cmp); - foreach ($this->deprecations[$group] as $msg => $notices) { - echo "\n ", $notices['count'], 'x: ', $msg, "\n"; + foreach ($notices as $msg => $notice) { + fwrite($handle, sprintf("\n %sx: %s\n", $notice->count(), $msg)); - arsort($notices); + $countsByCaller = $notice->getCountsByCaller(); + arsort($countsByCaller); - foreach ($notices as $method => $count) { + foreach ($countsByCaller as $method => $count) { if ('count' !== $method) { - echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n"; + fwrite($handle, sprintf(" %dx in %s\n", $count, preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method))); } } } @@ -330,7 +342,7 @@ private function displayDeprecations($groups, $configuration) } if (!empty($notices)) { - echo "\n"; + fwrite($handle, "\n"); } } diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php index d26ffc45de692..4420ef3d0e46c 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php @@ -31,18 +31,40 @@ class Configuration */ private $enabled = true; + /** + * @var bool[] + */ + private $verboseOutput; + /** * @var bool */ - private $verboseOutput = true; + private $generateBaseline = false; + + /** + * @var string + */ + private $baselineFile = ''; + + /** + * @var array + */ + private $baselineDeprecations = []; + + /** + * @var string|null + */ + private $logFile = null; /** - * @param int[] $thresholds A hash associating groups to thresholds - * @param string $regex Will be matched against messages, to decide - * whether to display a stack trace - * @param bool $verboseOutput + * @param int[] $thresholds A hash associating groups to thresholds + * @param string $regex Will be matched against messages, to decide whether to display a stack trace + * @param bool[] $verboseOutput Keyed by groups + * @param bool $generateBaseline Whether to generate or update the baseline file + * @param string $baselineFile The path to the baseline file + * @param string|null $logFile The path to the log file */ - private function __construct(array $thresholds = [], $regex = '', $verboseOutput = true) + private function __construct(array $thresholds = [], $regex = '', $verboseOutput = [], $generateBaseline = false, $baselineFile = '', $logFile = null) { $groups = ['total', 'indirect', 'direct', 'self']; @@ -72,7 +94,39 @@ private function __construct(array $thresholds = [], $regex = '', $verboseOutput } } $this->regex = $regex; - $this->verboseOutput = $verboseOutput; + + $this->verboseOutput = [ + 'unsilenced' => true, + 'direct' => true, + 'indirect' => true, + 'self' => true, + 'other' => true, + ]; + + foreach ($verboseOutput as $group => $status) { + if (!isset($this->verboseOutput[$group])) { + throw new \InvalidArgumentException(sprintf('Unsupported verbosity group "%s", expected one of "%s".', $group, implode('", "', array_keys($this->verboseOutput)))); + } + $this->verboseOutput[$group] = $status; + } + + if ($generateBaseline && !$baselineFile) { + throw new \InvalidArgumentException('You cannot use the "generateBaseline" configuration option without providing a "baselineFile" configuration option.'); + } + $this->generateBaseline = $generateBaseline; + $this->baselineFile = $baselineFile; + if ($this->baselineFile && !$this->generateBaseline) { + if (is_file($this->baselineFile)) { + $map = json_decode(file_get_contents($this->baselineFile)); + foreach ($map as $baseline_deprecation) { + $this->baselineDeprecations[$baseline_deprecation->location][$baseline_deprecation->message] = $baseline_deprecation->count; + } + } else { + throw new \InvalidArgumentException(sprintf('The baselineFile "%s" does not exist.', $this->baselineFile)); + } + } + + $this->logFile = $logFile; } /** @@ -84,24 +138,26 @@ public function isEnabled() } /** - * @param mixed[] $deprecations + * @param DeprecationGroup[] $deprecationGroups * * @return bool */ - public function tolerates(array $deprecations) + public function tolerates(array $deprecationGroups) { - $deprecationCounts = []; - foreach ($deprecations as $key => $deprecation) { - if (false !== strpos($key, 'Count') && false === strpos($key, 'legacy')) { - $deprecationCounts[$key] = $deprecation; + $grandTotal = 0; + + foreach ($deprecationGroups as $name => $group) { + if ('legacy' !== $name) { + $grandTotal += $group->count(); } } - if (array_sum($deprecationCounts) > $this->thresholds['total']) { + if ($grandTotal > $this->thresholds['total']) { return false; } + foreach (['self', 'direct', 'indirect'] as $deprecationType) { - if ($deprecationCounts['remaining '.$deprecationType.'Count'] > $this->thresholds[$deprecationType]) { + if ($deprecationGroups[$deprecationType]->count() > $this->thresholds[$deprecationType]) { return false; } } @@ -109,6 +165,61 @@ public function tolerates(array $deprecations) return true; } + /** + * @return bool + */ + public function isBaselineDeprecation(Deprecation $deprecation) + { + if ($deprecation->originatesFromAnObject()) { + $location = $deprecation->originatingClass().'::'.$deprecation->originatingMethod(); + } else { + $location = 'procedural code'; + } + + $message = $deprecation->getMessage(); + $result = isset($this->baselineDeprecations[$location][$message]) && $this->baselineDeprecations[$location][$message] > 0; + if ($this->generateBaseline) { + if ($result) { + ++$this->baselineDeprecations[$location][$message]; + } else { + $this->baselineDeprecations[$location][$message] = 1; + $result = true; + } + } elseif ($result) { + --$this->baselineDeprecations[$location][$message]; + } + + return $result; + } + + /** + * @return bool + */ + public function isGeneratingBaseline() + { + return $this->generateBaseline; + } + + public function getBaselineFile() + { + return $this->baselineFile; + } + + public function writeBaseline() + { + $map = []; + foreach ($this->baselineDeprecations as $location => $messages) { + foreach ($messages as $message => $count) { + $map[] = [ + 'location' => $location, + 'message' => $message, + 'count' => $count, + ]; + } + } + file_put_contents($this->baselineFile, json_encode($map, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + } + /** * @param string $message * @@ -130,9 +241,19 @@ public function isInRegexMode() /** * @return bool */ - public function verboseOutput() + public function verboseOutput($group) + { + return $this->verboseOutput[$group]; + } + + public function shouldWriteToLogFile() { - return $this->verboseOutput; + return null !== $this->logFile; + } + + public function getLogFile() + { + return $this->logFile; } /** @@ -145,24 +266,43 @@ public static function fromUrlEncodedString($serializedConfiguration) { parse_str($serializedConfiguration, $normalizedConfiguration); foreach (array_keys($normalizedConfiguration) as $key) { - if (!\in_array($key, ['max', 'disabled', 'verbose'], true)) { + if (!\in_array($key, ['max', 'disabled', 'verbose', 'quiet', 'generateBaseline', 'baselineFile', 'logFile'], true)) { throw new \InvalidArgumentException(sprintf('Unknown configuration option "%s".', $key)); } } - if (isset($normalizedConfiguration['disabled'])) { + $normalizedConfiguration += [ + 'max' => [], + 'disabled' => false, + 'verbose' => true, + 'quiet' => [], + 'generateBaseline' => false, + 'baselineFile' => '', + 'logFile' => null, + ]; + + if ('' === $normalizedConfiguration['disabled'] || filter_var($normalizedConfiguration['disabled'], \FILTER_VALIDATE_BOOLEAN)) { return self::inDisabledMode(); } - $verboseOutput = true; - if (isset($normalizedConfiguration['verbose'])) { - $verboseOutput = (bool) $normalizedConfiguration['verbose']; + $verboseOutput = []; + foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { + $verboseOutput[$group] = filter_var($normalizedConfiguration['verbose'], \FILTER_VALIDATE_BOOLEAN); + } + + if (\is_array($normalizedConfiguration['quiet'])) { + foreach ($normalizedConfiguration['quiet'] as $shushedGroup) { + $verboseOutput[$shushedGroup] = false; + } } return new self( - isset($normalizedConfiguration['max']) ? $normalizedConfiguration['max'] : [], + $normalizedConfiguration['max'] ?? [], '', - $verboseOutput + $verboseOutput, + filter_var($normalizedConfiguration['generateBaseline'], \FILTER_VALIDATE_BOOLEAN), + $normalizedConfiguration['baselineFile'], + $normalizedConfiguration['logFile'] ); } @@ -190,7 +330,12 @@ public static function inStrictMode() */ public static function inWeakMode() { - return new self([], '', false); + $verboseOutput = []; + foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { + $verboseOutput[$group] = false; + } + + return new self([], '', $verboseOutput); } /** diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php index d122de44090a8..5eda2bafdfb10 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php @@ -26,14 +26,14 @@ class_exists(Groups::class); */ class Deprecation { - const PATH_TYPE_VENDOR = 'path_type_vendor'; - const PATH_TYPE_SELF = 'path_type_internal'; - const PATH_TYPE_UNDETERMINED = 'path_type_undetermined'; + public const PATH_TYPE_VENDOR = 'path_type_vendor'; + public const PATH_TYPE_SELF = 'path_type_internal'; + public const PATH_TYPE_UNDETERMINED = 'path_type_undetermined'; - const TYPE_SELF = 'type_self'; - const TYPE_DIRECT = 'type_direct'; - const TYPE_INDIRECT = 'type_indirect'; - const TYPE_UNDETERMINED = 'type_undetermined'; + public const TYPE_SELF = 'type_self'; + public const TYPE_DIRECT = 'type_direct'; + public const TYPE_INDIRECT = 'type_indirect'; + public const TYPE_UNDETERMINED = 'type_undetermined'; private $trace = []; private $message; @@ -59,6 +59,11 @@ class Deprecation */ public function __construct($message, array $trace, $file) { + if (isset($trace[2]['function']) && 'trigger_deprecation' === $trace[2]['function']) { + $file = $trace[2]['file']; + array_splice($trace, 1, 1); + } + $this->trace = $trace; $this->message = $message; @@ -82,7 +87,7 @@ public function __construct($message, array $trace, $file) $this->getOriginalFilesStack(); array_splice($this->originalFilesStack, 0, $j, [$this->triggeringFile]); - if (preg_match('/(?|"([^"]++)" that is deprecated|should implement method "(?:static )?([^:]++))/', $message, $m) || preg_match('/^(?:The|Method) "([^":]++)/', $message, $m)) { + if (preg_match('/(?|"([^"]++)" that is deprecated|should implement method "(?:static )?([^:]++))/', $message, $m) || (false === strpos($message, '()" will return') && false === strpos($message, 'native return type declaration') && preg_match('/^(?:The|Method) "([^":]++)/', $message, $m))) { $this->triggeringFile = (new \ReflectionClass($m[1]))->getFileName(); array_unshift($this->originalFilesStack, $this->triggeringFile); } @@ -96,6 +101,30 @@ public function __construct($message, array $trace, $file) return; } + set_error_handler(function () {}); + try { + $parsedMsg = unserialize($this->message); + } finally { + restore_error_handler(); + } + if ($parsedMsg && isset($parsedMsg['deprecation'])) { + $this->message = $parsedMsg['deprecation']; + $this->originClass = $parsedMsg['class']; + $this->originMethod = $parsedMsg['method']; + if (isset($parsedMsg['files_stack'])) { + $this->originalFilesStack = $parsedMsg['files_stack']; + } + // If the deprecation has been triggered via + // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() + // then we need to use the serialized information to determine + // if the error has been triggered from vendor code. + if (isset($parsedMsg['triggering_file'])) { + $this->triggeringFile = $parsedMsg['triggering_file']; + } + + return; + } + if (!isset($line['class'], $trace[$i - 2]['function']) || 0 !== strpos($line['class'], SymfonyTestsListenerFor::class)) { $this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class']; $this->originMethod = $line['function']; @@ -103,7 +132,7 @@ public function __construct($message, array $trace, $file) return; } - $test = isset($line['args'][0]) ? $line['args'][0] : null; + $test = $line['args'][0] ?? null; if (($test instanceof TestCase || $test instanceof TestSuite) && ('trigger_error' !== $trace[$i - 2]['function'] || isset($trace[$i - 2]['class']))) { $this->originClass = \get_class($test); @@ -111,23 +140,6 @@ public function __construct($message, array $trace, $file) return; } - - set_error_handler(function () {}); - $parsedMsg = unserialize($this->message); - restore_error_handler(); - $this->message = $parsedMsg['deprecation']; - $this->originClass = $parsedMsg['class']; - $this->originMethod = $parsedMsg['method']; - if (isset($parsedMsg['files_stack'])) { - $this->originalFilesStack = $parsedMsg['files_stack']; - } - // If the deprecation has been triggered via - // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() - // then we need to use the serialized information to determine - // if the error has been triggered from vendor code. - if (isset($parsedMsg['triggering_file'])) { - $this->triggeringFile = $parsedMsg['triggering_file']; - } } /** @@ -140,7 +152,7 @@ private function lineShouldBeSkipped(array $line) } $class = $line['class']; - return 'ReflectionMethod' === $class || 0 === strpos($class, 'PHPUnit_') || 0 === strpos($class, 'PHPUnit\\'); + return 'ReflectionMethod' === $class || 0 === strpos($class, 'PHPUnit\\'); } /** @@ -301,7 +313,7 @@ private function getPackage($path) } /** - * @return string[] an array of paths + * @return string[] */ private static function getVendors() { @@ -317,7 +329,7 @@ private static function getVendors() foreach (get_declared_classes() as $class) { if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); - $v = \dirname(\dirname($r->getFileName())); + $v = \dirname($r->getFileName(), 2); if (file_exists($v.'/composer/installed.json')) { self::$vendors[] = $v; $loader = require $v.'/autoload.php'; diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationGroup.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationGroup.php new file mode 100644 index 0000000000000..6ad2b84ea3fd6 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationGroup.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; + +/** + * @internal + */ +final class DeprecationGroup +{ + private $count = 0; + + /** + * @var DeprecationNotice[] keys are messages + */ + private $deprecationNotices = []; + + /** + * @param string $message + * @param string $class + * @param string $method + */ + public function addNoticeFromObject($message, $class, $method) + { + $this->deprecationNotice($message)->addObjectOccurrence($class, $method); + $this->addNotice(); + } + + /** + * @param string $message + */ + public function addNoticeFromProceduralCode($message) + { + $this->deprecationNotice($message)->addProceduralOccurrence(); + $this->addNotice(); + } + + public function addNotice() + { + ++$this->count; + } + + /** + * @param string $message + * + * @return DeprecationNotice + */ + private function deprecationNotice($message) + { + return $this->deprecationNotices[$message] ?? $this->deprecationNotices[$message] = new DeprecationNotice(); + } + + public function count() + { + return $this->count; + } + + public function notices() + { + return $this->deprecationNotices; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationNotice.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationNotice.php new file mode 100644 index 0000000000000..854bbd4d26333 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationNotice.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; + +/** + * @internal + */ +final class DeprecationNotice +{ + private $count = 0; + + /** + * @var int[] + */ + private $countsByCaller = []; + + public function addObjectOccurrence($class, $method) + { + if (!isset($this->countsByCaller["$class::$method"])) { + $this->countsByCaller["$class::$method"] = 0; + } + ++$this->countsByCaller["$class::$method"]; + ++$this->count; + } + + public function addProceduralOccurrence() + { + ++$this->count; + } + + public function getCountsByCaller() + { + return $this->countsByCaller; + } + + public function count() + { + return $this->count; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/DnsMock.php b/src/Symfony/Bridge/PhpUnit/DnsMock.php index 1e2f55b371be3..642da0a6dfcde 100644 --- a/src/Symfony/Bridge/PhpUnit/DnsMock.php +++ b/src/Symfony/Bridge/PhpUnit/DnsMock.php @@ -152,7 +152,7 @@ public static function dns_get_record($hostname, $type = \DNS_ANY, &$authns = nu $records = []; foreach (self::$hosts[$hostname] as $record) { - if (isset(self::$dnsTypes[$record['type']]) && (self::$dnsTypes[$record['type']] & $type)) { + if ((self::$dnsTypes[$record['type']] ?? 0) & $type) { $records[] = array_merge(['host' => $hostname, 'class' => 'IN', 'ttl' => 1, 'type' => $record['type']], $record); } } @@ -163,7 +163,7 @@ public static function dns_get_record($hostname, $type = \DNS_ANY, &$authns = nu public static function register($class) { - $self = \get_called_class(); + $self = static::class; $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; if (0 < strpos($class, '\\Tests\\')) { diff --git a/src/Symfony/Bridge/PhpUnit/ExpectDeprecationTrait.php b/src/Symfony/Bridge/PhpUnit/ExpectDeprecationTrait.php new file mode 100644 index 0000000000000..fd7f2c80b84f9 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/ExpectDeprecationTrait.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +use Symfony\Bridge\PhpUnit\Legacy\ExpectDeprecationTraitBeforeV8_4; +use Symfony\Bridge\PhpUnit\Legacy\ExpectDeprecationTraitForV8_4; + +if (version_compare(\PHPUnit\Runner\Version::id(), '8.4.0', '<')) { + trait ExpectDeprecationTrait + { + use ExpectDeprecationTraitBeforeV8_4; + } +} else { + /** + * @method void expectDeprecation(string $message) + */ + trait ExpectDeprecationTrait + { + use ExpectDeprecationTraitForV8_4; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php deleted file mode 100644 index 2ce390df38609..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -/** - * {@inheritdoc} - * - * @internal - */ -class CommandForV5 extends \PHPUnit_TextUI_Command -{ - /** - * {@inheritdoc} - */ - protected function createRunner() - { - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; - - $registeredLocally = false; - - foreach ($this->arguments['listeners'] as $registeredListener) { - if ($registeredListener instanceof SymfonyTestsListenerForV5) { - $registeredListener->globalListenerDisabled(); - $registeredLocally = true; - break; - } - } - - if (isset($this->arguments['configuration'])) { - $configuration = $this->arguments['configuration']; - if (!$configuration instanceof \PHPUnit_Util_Configuration) { - $configuration = \PHPUnit_Util_Configuration::getInstance($this->arguments['configuration']); - } - foreach ($configuration->getListenerConfiguration() as $registeredListener) { - if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { - $registeredLocally = true; - break; - } - } - } - - if (!$registeredLocally) { - $this->arguments['listeners'][] = new SymfonyTestsListenerForV5(); - } - - return parent::createRunner(); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php deleted file mode 100644 index 93e1ad975b7e4..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use PHPUnit\TextUI\Command as BaseCommand; -use PHPUnit\TextUI\TestRunner as BaseRunner; -use PHPUnit\Util\Configuration; -use Symfony\Bridge\PhpUnit\SymfonyTestsListener; - -/** - * {@inheritdoc} - * - * @internal - */ -class CommandForV6 extends BaseCommand -{ - /** - * {@inheritdoc} - */ - protected function createRunner(): BaseRunner - { - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; - - $registeredLocally = false; - - foreach ($this->arguments['listeners'] as $registeredListener) { - if ($registeredListener instanceof SymfonyTestsListener) { - $registeredListener->globalListenerDisabled(); - $registeredLocally = true; - break; - } - } - - if (isset($this->arguments['configuration'])) { - $configuration = $this->arguments['configuration']; - if (!$configuration instanceof Configuration) { - $configuration = Configuration::getInstance($this->arguments['configuration']); - } - foreach ($configuration->getListenerConfiguration() as $registeredListener) { - if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { - $registeredLocally = true; - break; - } - } - } - - if (!$registeredLocally) { - $this->arguments['listeners'][] = new SymfonyTestsListener(); - } - - return parent::createRunner(); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV7.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV7.php new file mode 100644 index 0000000000000..fcf5c4505d3da --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV7.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +use PHPUnit\TextUI\Command as BaseCommand; +use PHPUnit\TextUI\TestRunner as BaseRunner; +use PHPUnit\Util\Configuration; +use Symfony\Bridge\PhpUnit\SymfonyTestsListener; + +/** + * {@inheritdoc} + * + * @internal + */ +class CommandForV7 extends BaseCommand +{ + /** + * {@inheritdoc} + */ + protected function createRunner(): BaseRunner + { + $this->arguments['listeners'] ?? $this->arguments['listeners'] = []; + + $registeredLocally = false; + + foreach ($this->arguments['listeners'] as $registeredListener) { + if ($registeredListener instanceof SymfonyTestsListener) { + $registeredListener->globalListenerDisabled(); + $registeredLocally = true; + break; + } + } + + if (isset($this->arguments['configuration'])) { + $configuration = $this->arguments['configuration']; + if (!$configuration instanceof Configuration) { + $configuration = Configuration::getInstance($this->arguments['configuration']); + } + foreach ($configuration->getListenerConfiguration() as $registeredListener) { + if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { + $registeredLocally = true; + break; + } + } + } + + if (!$registeredLocally) { + $this->arguments['listeners'][] = new SymfonyTestsListener(); + } + + return parent::createRunner(); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.php index 2511380257fd8..351f02f2230ec 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.php @@ -31,7 +31,7 @@ class CommandForV9 extends BaseCommand */ protected function createRunner(): BaseRunner { - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; + $this->arguments['listeners'] ?? $this->arguments['listeners'] = []; $registeredLocally = false; diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV6.php deleted file mode 100644 index 53819e4b3c4d7..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV6.php +++ /dev/null @@ -1,130 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use SebastianBergmann\Exporter\Exporter; - -/** - * @internal - */ -trait ConstraintTraitForV6 -{ - /** - * @return bool|null - */ - public function evaluate($other, $description = '', $returnResult = false) - { - return $this->doEvaluate($other, $description, $returnResult); - } - - /** - * @return int - */ - public function count() - { - return $this->doCount(); - } - - /** - * @return string - */ - public function toString() - { - return $this->doToString(); - } - - /** - * @param mixed $other - * - * @return string - */ - protected function additionalFailureDescription($other) - { - return $this->doAdditionalFailureDescription($other); - } - - /** - * @return Exporter - */ - protected function exporter() - { - if (null === $this->exporter) { - $this->exporter = new Exporter(); - } - - return $this->exporter; - } - - /** - * @param mixed $other - * - * @return string - */ - protected function failureDescription($other) - { - return $this->doFailureDescription($other); - } - - /** - * @param mixed $other - * - * @return bool - */ - protected function matches($other) - { - return $this->doMatches($other); - } - - private function doAdditionalFailureDescription($other) - { - return ''; - } - - private function doCount() - { - return 1; - } - - private function doEvaluate($other, $description, $returnResult) - { - $success = false; - - if ($this->matches($other)) { - $success = true; - } - - if ($returnResult) { - return $success; - } - - if (!$success) { - $this->fail($other, $description); - } - - return null; - } - - private function doFailureDescription($other) - { - return $this->exporter()->export($other).' '.$this->toString(); - } - - private function doMatches($other) - { - return false; - } - - private function doToString() - { - return ''; - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV5.php deleted file mode 100644 index 9d754eebc85df..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV5.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -/** - * CoverageListener adds `@covers ` on each test when possible to - * make the code coverage more accurate. - * - * @author Grégoire Pineau - * - * @internal - */ -class CoverageListenerForV5 extends \PHPUnit_Framework_BaseTestListener -{ - private $trait; - - public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) - { - $this->trait = new CoverageListenerTrait($sutFqcnResolver, $warningOnSutNotFound); - } - - public function startTest(\PHPUnit_Framework_Test $test) - { - $this->trait->startTest($test); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php deleted file mode 100644 index 1b3ceec161f8a..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestListener; -use PHPUnit\Framework\TestListenerDefaultImplementation; - -/** - * CoverageListener adds `@covers ` on each test when possible to - * make the code coverage more accurate. - * - * @author Grégoire Pineau - * - * @internal - */ -class CoverageListenerForV6 implements TestListener -{ - use TestListenerDefaultImplementation; - - private $trait; - - public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) - { - $this->trait = new CoverageListenerTrait($sutFqcnResolver, $warningOnSutNotFound); - } - - public function startTest(Test $test) - { - $this->trait->startTest($test); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV7.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV7.php deleted file mode 100644 index a35034c48b32b..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV7.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestListener; -use PHPUnit\Framework\TestListenerDefaultImplementation; - -/** - * CoverageListener adds `@covers ` on each test when possible to - * make the code coverage more accurate. - * - * @author Grégoire Pineau - * - * @internal - */ -class CoverageListenerForV7 implements TestListener -{ - use TestListenerDefaultImplementation; - - private $trait; - - public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) - { - $this->trait = new CoverageListenerTrait($sutFqcnResolver, $warningOnSutNotFound); - } - - public function startTest(Test $test): void - { - $this->trait->startTest($test); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php deleted file mode 100644 index 4ca396ece164b..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php +++ /dev/null @@ -1,160 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Warning; -use PHPUnit\Util\Annotation\Registry; -use PHPUnit\Util\Test; - -/** - * PHP 5.3 compatible trait-like shared implementation. - * - * @author Grégoire Pineau - * - * @internal - */ -class CoverageListenerTrait -{ - private $sutFqcnResolver; - private $warningOnSutNotFound; - private $warnings; - - public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) - { - $this->sutFqcnResolver = $sutFqcnResolver; - $this->warningOnSutNotFound = $warningOnSutNotFound; - $this->warnings = []; - } - - public function startTest($test) - { - if (!$test instanceof TestCase) { - return; - } - - $annotations = Test::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); - - $ignoredAnnotations = ['covers', 'coversDefaultClass', 'coversNothing']; - - foreach ($ignoredAnnotations as $annotation) { - if (isset($annotations['class'][$annotation]) || isset($annotations['method'][$annotation])) { - return; - } - } - - $sutFqcn = $this->findSutFqcn($test); - if (!$sutFqcn) { - if ($this->warningOnSutNotFound) { - $message = 'Could not find the tested class.'; - // addWarning does not exist on old PHPUnit version - if (method_exists($test->getTestResultObject(), 'addWarning') && class_exists(Warning::class)) { - $test->getTestResultObject()->addWarning($test, new Warning($message), 0); - } else { - $this->warnings[] = sprintf("%s::%s\n%s", \get_class($test), $test->getName(), $message); - } - } - - return; - } - - $covers = $sutFqcn; - if (!\is_array($sutFqcn)) { - $covers = [$sutFqcn]; - while ($parent = get_parent_class($sutFqcn)) { - $covers[] = $parent; - $sutFqcn = $parent; - } - } - - if (class_exists(Registry::class)) { - $this->addCoversForDocBlockInsideRegistry($test, $covers); - - return; - } - - $this->addCoversForClassToAnnotationCache($test, $covers); - } - - private function addCoversForClassToAnnotationCache($test, $covers) - { - $r = new \ReflectionProperty(Test::class, 'annotationCache'); - $r->setAccessible(true); - - $cache = $r->getValue(); - $cache = array_replace_recursive($cache, [ - \get_class($test) => [ - 'covers' => $covers, - ], - ]); - - $r->setValue(Test::class, $cache); - } - - private function addCoversForDocBlockInsideRegistry($test, $covers) - { - $docBlock = Registry::getInstance()->forClassName(\get_class($test)); - - $symbolAnnotations = new \ReflectionProperty($docBlock, 'symbolAnnotations'); - $symbolAnnotations->setAccessible(true); - - // Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException - $covers = array_filter($covers, function ($class) { - $reflector = new \ReflectionClass($class); - - return $reflector->isUserDefined(); - }); - - $symbolAnnotations->setValue($docBlock, array_replace($docBlock->symbolAnnotations(), [ - 'covers' => $covers, - ])); - } - - private function findSutFqcn($test) - { - if ($this->sutFqcnResolver) { - $resolver = $this->sutFqcnResolver; - - return $resolver($test); - } - - $class = \get_class($test); - - $sutFqcn = str_replace('\\Tests\\', '\\', $class); - $sutFqcn = preg_replace('{Test$}', '', $sutFqcn); - - return class_exists($sutFqcn) ? $sutFqcn : null; - } - - public function __sleep() - { - throw new \BadMethodCallException('Cannot serialize '.__CLASS__); - } - - public function __wakeup() - { - throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); - } - - public function __destruct() - { - if (!$this->warnings) { - return; - } - - echo "\n"; - - foreach ($this->warnings as $key => $warning) { - echo sprintf("%d) %s\n", ++$key, $warning); - } - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ExpectDeprecationTraitBeforeV8_4.php b/src/Symfony/Bridge/PhpUnit/Legacy/ExpectDeprecationTraitBeforeV8_4.php new file mode 100644 index 0000000000000..03368b5597fbe --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ExpectDeprecationTraitBeforeV8_4.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +/** + * @internal, use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait instead. + */ +trait ExpectDeprecationTraitBeforeV8_4 +{ + /** + * @param string $message + * + * @return void + */ + protected function expectDeprecation($message) + { + // Expected deprecations set by isolated tests need to be written to a file + // so that the test running process can take account of them. + if ($file = getenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE')) { + $this->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); + $expectedDeprecations = file_get_contents($file); + if ($expectedDeprecations) { + $expectedDeprecations = array_merge(unserialize($expectedDeprecations), [$message]); + } else { + $expectedDeprecations = [$message]; + } + file_put_contents($file, serialize($expectedDeprecations)); + + return; + } + + if (!SymfonyTestsListenerTrait::$previousErrorHandler) { + SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']); + } + + SymfonyTestsListenerTrait::$expectedDeprecations[] = $message; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ExpectDeprecationTraitForV8_4.php b/src/Symfony/Bridge/PhpUnit/Legacy/ExpectDeprecationTraitForV8_4.php new file mode 100644 index 0000000000000..d15963520d6f2 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ExpectDeprecationTraitForV8_4.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +/** + * @internal use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait instead + */ +trait ExpectDeprecationTraitForV8_4 +{ + /** + * @param string $message + */ + public function expectDeprecation(): void + { + if (1 > \func_num_args() || !\is_string($message = func_get_arg(0))) { + throw new \InvalidArgumentException(sprintf('The "%s()" method requires the string $message argument.', __FUNCTION__)); + } + + // Expected deprecations set by isolated tests need to be written to a file + // so that the test running process can take account of them. + if ($file = getenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE')) { + $this->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); + $expectedDeprecations = file_get_contents($file); + if ($expectedDeprecations) { + $expectedDeprecations = array_merge(unserialize($expectedDeprecations), [$message]); + } else { + $expectedDeprecations = [$message]; + } + file_put_contents($file, serialize($expectedDeprecations)); + + return; + } + + if (!SymfonyTestsListenerTrait::$previousErrorHandler) { + SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']); + } + + SymfonyTestsListenerTrait::$expectedDeprecations[] = $message; + } + + /** + * @internal use expectDeprecation() instead + */ + public function expectDeprecationMessage(string $message): void + { + throw new \BadMethodCallException(sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait, pass the message to expectDeprecation() instead.', __FUNCTION__)); + } + + /** + * @internal use expectDeprecation() instead + */ + public function expectDeprecationMessageMatches(string $regularExpression): void + { + throw new \BadMethodCallException(sprintf('The "%s()" method is not supported by Symfony\'s PHPUnit Bridge ExpectDeprecationTrait.', __FUNCTION__)); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php index 5a66282d855ca..7424b7226ea14 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php @@ -11,9 +11,7 @@ namespace Symfony\Bridge\PhpUnit\Legacy; -use PHPUnit\Framework\Constraint\IsEqual; use PHPUnit\Framework\Constraint\LogicalNot; -use PHPUnit\Framework\Constraint\StringContains; use PHPUnit\Framework\Constraint\TraversableContains; /** @@ -21,18 +19,6 @@ */ trait PolyfillAssertTrait { - /** - * @param float $delta - * @param string $message - * - * @return void - */ - public static function assertEqualsWithDelta($expected, $actual, $delta, $message = '') - { - $constraint = new IsEqual($expected, $delta); - static::assertThat($actual, $constraint, $message); - } - /** * @param iterable $haystack * @param string $message @@ -57,225 +43,6 @@ public static function assertNotContainsEquals($needle, $haystack, $message = '' static::assertThat($haystack, $constraint, $message); } - /** - * @param string $message - * - * @return void - */ - public static function assertIsArray($actual, $message = '') - { - static::assertInternalType('array', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsBool($actual, $message = '') - { - static::assertInternalType('bool', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsFloat($actual, $message = '') - { - static::assertInternalType('float', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsInt($actual, $message = '') - { - static::assertInternalType('int', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsNumeric($actual, $message = '') - { - static::assertInternalType('numeric', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsObject($actual, $message = '') - { - static::assertInternalType('object', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsResource($actual, $message = '') - { - static::assertInternalType('resource', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsString($actual, $message = '') - { - static::assertInternalType('string', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsScalar($actual, $message = '') - { - static::assertInternalType('scalar', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsCallable($actual, $message = '') - { - static::assertInternalType('callable', $actual, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertIsIterable($actual, $message = '') - { - static::assertInternalType('iterable', $actual, $message); - } - - /** - * @param string $needle - * @param string $haystack - * @param string $message - * - * @return void - */ - public static function assertStringContainsString($needle, $haystack, $message = '') - { - $constraint = new StringContains($needle, false); - static::assertThat($haystack, $constraint, $message); - } - - /** - * @param string $needle - * @param string $haystack - * @param string $message - * - * @return void - */ - public static function assertStringContainsStringIgnoringCase($needle, $haystack, $message = '') - { - $constraint = new StringContains($needle, true); - static::assertThat($haystack, $constraint, $message); - } - - /** - * @param string $needle - * @param string $haystack - * @param string $message - * - * @return void - */ - public static function assertStringNotContainsString($needle, $haystack, $message = '') - { - $constraint = new LogicalNot(new StringContains($needle, false)); - static::assertThat($haystack, $constraint, $message); - } - - /** - * @param string $needle - * @param string $haystack - * @param string $message - * - * @return void - */ - public static function assertStringNotContainsStringIgnoringCase($needle, $haystack, $message = '') - { - $constraint = new LogicalNot(new StringContains($needle, true)); - static::assertThat($haystack, $constraint, $message); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertFinite($actual, $message = '') - { - static::assertInternalType('float', $actual, $message); - static::assertTrue(is_finite($actual), $message ?: "Failed asserting that $actual is finite."); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertInfinite($actual, $message = '') - { - static::assertInternalType('float', $actual, $message); - static::assertTrue(is_infinite($actual), $message ?: "Failed asserting that $actual is infinite."); - } - - /** - * @param string $message - * - * @return void - */ - public static function assertNan($actual, $message = '') - { - static::assertInternalType('float', $actual, $message); - static::assertTrue(is_nan($actual), $message ?: "Failed asserting that $actual is nan."); - } - - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertIsReadable($filename, $message = '') - { - static::assertInternalType('string', $filename, $message); - static::assertTrue(is_readable($filename), $message ?: "Failed asserting that $filename is readable."); - } - - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertNotIsReadable($filename, $message = '') - { - static::assertInternalType('string', $filename, $message); - static::assertFalse(is_readable($filename), $message ?: "Failed asserting that $filename is not readable."); - } - /** * @param string $filename * @param string $message @@ -287,30 +54,6 @@ public static function assertIsNotReadable($filename, $message = '') static::assertNotIsReadable($filename, $message); } - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertIsWritable($filename, $message = '') - { - static::assertInternalType('string', $filename, $message); - static::assertTrue(is_writable($filename), $message ?: "Failed asserting that $filename is writable."); - } - - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertNotIsWritable($filename, $message = '') - { - static::assertInternalType('string', $filename, $message); - static::assertFalse(is_writable($filename), $message ?: "Failed asserting that $filename is not writable."); - } - /** * @param string $filename * @param string $message @@ -322,30 +65,6 @@ public static function assertIsNotWritable($filename, $message = '') static::assertNotIsWritable($filename, $message); } - /** - * @param string $directory - * @param string $message - * - * @return void - */ - public static function assertDirectoryExists($directory, $message = '') - { - static::assertInternalType('string', $directory, $message); - static::assertTrue(is_dir($directory), $message ?: "Failed asserting that $directory exists."); - } - - /** - * @param string $directory - * @param string $message - * - * @return void - */ - public static function assertDirectoryNotExists($directory, $message = '') - { - static::assertInternalType('string', $directory, $message); - static::assertFalse(is_dir($directory), $message ?: "Failed asserting that $directory does not exist."); - } - /** * @param string $directory * @param string $message @@ -357,30 +76,6 @@ public static function assertDirectoryDoesNotExist($directory, $message = '') static::assertDirectoryNotExists($directory, $message); } - /** - * @param string $directory - * @param string $message - * - * @return void - */ - public static function assertDirectoryIsReadable($directory, $message = '') - { - static::assertDirectoryExists($directory, $message); - static::assertIsReadable($directory, $message); - } - - /** - * @param string $directory - * @param string $message - * - * @return void - */ - public static function assertDirectoryNotIsReadable($directory, $message = '') - { - static::assertDirectoryExists($directory, $message); - static::assertNotIsReadable($directory, $message); - } - /** * @param string $directory * @param string $message @@ -392,30 +87,6 @@ public static function assertDirectoryIsNotReadable($directory, $message = '') static::assertDirectoryNotIsReadable($directory, $message); } - /** - * @param string $directory - * @param string $message - * - * @return void - */ - public static function assertDirectoryIsWritable($directory, $message = '') - { - static::assertDirectoryExists($directory, $message); - static::assertIsWritable($directory, $message); - } - - /** - * @param string $directory - * @param string $message - * - * @return void - */ - public static function assertDirectoryNotIsWritable($directory, $message = '') - { - static::assertDirectoryExists($directory, $message); - static::assertNotIsWritable($directory, $message); - } - /** * @param string $directory * @param string $message @@ -427,30 +98,6 @@ public static function assertDirectoryIsNotWritable($directory, $message = '') static::assertDirectoryNotIsWritable($directory, $message); } - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertFileExists($filename, $message = '') - { - static::assertInternalType('string', $filename, $message); - static::assertTrue(file_exists($filename), $message ?: "Failed asserting that $filename exists."); - } - - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertFileNotExists($filename, $message = '') - { - static::assertInternalType('string', $filename, $message); - static::assertFalse(file_exists($filename), $message ?: "Failed asserting that $filename does not exist."); - } - /** * @param string $filename * @param string $message @@ -462,30 +109,6 @@ public static function assertFileDoesNotExist($filename, $message = '') static::assertFileNotExists($filename, $message); } - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertFileIsReadable($filename, $message = '') - { - static::assertFileExists($filename, $message); - static::assertIsReadable($filename, $message); - } - - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertFileNotIsReadable($filename, $message = '') - { - static::assertFileExists($filename, $message); - static::assertNotIsReadable($filename, $message); - } - /** * @param string $filename * @param string $message @@ -497,30 +120,6 @@ public static function assertFileIsNotReadable($filename, $message = '') static::assertFileNotIsReadable($filename, $message); } - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertFileIsWritable($filename, $message = '') - { - static::assertFileExists($filename, $message); - static::assertIsWritable($filename, $message); - } - - /** - * @param string $filename - * @param string $message - * - * @return void - */ - public static function assertFileNotIsWritable($filename, $message = '') - { - static::assertFileExists($filename, $message); - static::assertNotIsWritable($filename, $message); - } - /** * @param string $filename * @param string $message diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.php index ad2150436833d..8673bdc0a1d2b 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.php @@ -14,88 +14,12 @@ use PHPUnit\Framework\Error\Error; use PHPUnit\Framework\Error\Notice; use PHPUnit\Framework\Error\Warning; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; /** * This trait is @internal. */ trait PolyfillTestCaseTrait { - /** - * @param string|string[] $originalClassName - * - * @return MockObject - */ - protected function createMock($originalClassName) - { - $mock = $this->getMockBuilder($originalClassName) - ->disableOriginalConstructor() - ->disableOriginalClone() - ->disableArgumentCloning(); - - if (method_exists($mock, 'disallowMockingUnknownTypes')) { - $mock = $mock->disallowMockingUnknownTypes(); - } - - return $mock->getMock(); - } - - /** - * @param string|string[] $originalClassName - * @param string[] $methods - * - * @return MockObject - */ - protected function createPartialMock($originalClassName, array $methods) - { - $mock = $this->getMockBuilder($originalClassName) - ->disableOriginalConstructor() - ->disableOriginalClone() - ->disableArgumentCloning() - ->setMethods(empty($methods) ? null : $methods); - - if (method_exists($mock, 'disallowMockingUnknownTypes')) { - $mock = $mock->disallowMockingUnknownTypes(); - } - - return $mock->getMock(); - } - - /** - * @param string $exception - * - * @return void - */ - public function expectException($exception) - { - $this->doExpectException($exception); - } - - /** - * @param int|string $code - * - * @return void - */ - public function expectExceptionCode($code) - { - $property = new \ReflectionProperty(TestCase::class, 'expectedExceptionCode'); - $property->setAccessible(true); - $property->setValue($this, $code); - } - - /** - * @param string $message - * - * @return void - */ - public function expectExceptionMessage($message) - { - $property = new \ReflectionProperty(TestCase::class, 'expectedExceptionMessage'); - $property->setAccessible(true); - $property->setValue($this, $message); - } - /** * @param string $messageRegExp * @@ -106,24 +30,12 @@ public function expectExceptionMessageMatches($messageRegExp) $this->expectExceptionMessageRegExp($messageRegExp); } - /** - * @param string $messageRegExp - * - * @return void - */ - public function expectExceptionMessageRegExp($messageRegExp) - { - $property = new \ReflectionProperty(TestCase::class, 'expectedExceptionMessageRegExp'); - $property->setAccessible(true); - $property->setValue($this, $messageRegExp); - } - /** * @return void */ public function expectNotice() { - $this->doExpectException(Notice::class); + $this->expectException(Notice::class); } /** @@ -151,7 +63,7 @@ public function expectNoticeMessageMatches($regularExpression) */ public function expectWarning() { - $this->doExpectException(Warning::class); + $this->expectException(Warning::class); } /** @@ -179,7 +91,7 @@ public function expectWarningMessageMatches($regularExpression) */ public function expectError() { - $this->doExpectException(Error::class); + $this->expectException(Error::class); } /** @@ -201,11 +113,4 @@ public function expectErrorMessageMatches($regularExpression) { $this->expectExceptionMessageMatches($regularExpression); } - - private function doExpectException($exception) - { - $property = new \ReflectionProperty(TestCase::class, 'expectedException'); - $property->setAccessible(true); - $property->setValue($this, $exception); - } } diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV5.php deleted file mode 100644 index ca29c2ae49ab8..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV5.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -/** - * @internal - */ -trait SetUpTearDownTraitForV5 -{ - /** - * @return void - */ - public static function setUpBeforeClass() - { - self::doSetUpBeforeClass(); - } - - /** - * @return void - */ - public static function tearDownAfterClass() - { - self::doTearDownAfterClass(); - } - - /** - * @return void - */ - protected function setUp() - { - self::doSetUp(); - } - - /** - * @return void - */ - protected function tearDown() - { - self::doTearDown(); - } - - private static function doSetUpBeforeClass() - { - parent::setUpBeforeClass(); - } - - private static function doTearDownAfterClass() - { - parent::tearDownAfterClass(); - } - - private function doSetUp() - { - parent::setUp(); - } - - private function doTearDown() - { - parent::tearDown(); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV8.php b/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV8.php deleted file mode 100644 index cc81df281880a..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV8.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -/** - * @internal - */ -trait SetUpTearDownTraitForV8 -{ - public static function setUpBeforeClass(): void - { - self::doSetUpBeforeClass(); - } - - public static function tearDownAfterClass(): void - { - self::doTearDownAfterClass(); - } - - protected function setUp(): void - { - self::doSetUp(); - } - - protected function tearDown(): void - { - self::doTearDown(); - } - - private static function doSetUpBeforeClass(): void - { - parent::setUpBeforeClass(); - } - - private static function doTearDownAfterClass(): void - { - parent::tearDownAfterClass(); - } - - private function doSetUp(): void - { - parent::setUp(); - } - - private function doTearDown(): void - { - parent::tearDown(); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php deleted file mode 100644 index 9b646dca8dfab..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -/** - * Collects and replays skipped tests. - * - * @author Nicolas Grekas - * - * @internal - */ -class SymfonyTestsListenerForV5 extends \PHPUnit_Framework_BaseTestListener -{ - private $trait; - - public function __construct(array $mockedNamespaces = []) - { - $this->trait = new SymfonyTestsListenerTrait($mockedNamespaces); - } - - public function globalListenerDisabled() - { - $this->trait->globalListenerDisabled(); - } - - public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) - { - $this->trait->startTestSuite($suite); - } - - public function addSkippedTest(\PHPUnit_Framework_Test $test, \Exception $e, $time) - { - $this->trait->addSkippedTest($test, $e, $time); - } - - public function startTest(\PHPUnit_Framework_Test $test) - { - $this->trait->startTest($test); - } - - public function endTest(\PHPUnit_Framework_Test $test, $time) - { - $this->trait->endTest($test, $time); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php deleted file mode 100644 index 8f2f6b5a7ed54..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Legacy; - -use PHPUnit\Framework\BaseTestListener; -use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestSuite; - -/** - * Collects and replays skipped tests. - * - * @author Nicolas Grekas - * - * @internal - */ -class SymfonyTestsListenerForV6 extends BaseTestListener -{ - private $trait; - - public function __construct(array $mockedNamespaces = []) - { - $this->trait = new SymfonyTestsListenerTrait($mockedNamespaces); - } - - public function globalListenerDisabled() - { - $this->trait->globalListenerDisabled(); - } - - public function startTestSuite(TestSuite $suite) - { - $this->trait->startTestSuite($suite); - } - - public function addSkippedTest(Test $test, \Exception $e, $time) - { - $this->trait->addSkippedTest($test, $e, $time); - } - - public function startTest(Test $test) - { - $this->trait->startTest($test); - } - - public function endTest(Test $test, $time) - { - $this->trait->endTest($test, $time); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index 0f9238bdd9c1c..c84ec1b66ddbd 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -13,6 +13,7 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\RiskyTestError; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\BaseTestRunner; @@ -21,6 +22,7 @@ use PHPUnit\Util\Test; use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Bridge\PhpUnit\DnsMock; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; use Symfony\Component\ErrorHandler\DebugClassLoader; @@ -33,16 +35,16 @@ */ class SymfonyTestsListenerTrait { + public static $expectedDeprecations = []; + public static $previousErrorHandler; + private static $gatheredDeprecations = []; private static $globallyEnabled = false; private $state = -1; private $skippedFile = false; private $wasSkipped = []; private $isSkipped = []; - private $expectedDeprecations = []; - private $gatheredDeprecations = []; - private $previousErrorHandler; - private $error; private $runsInSeparateProcess = false; + private $checkNumAssertions = false; /** * @param array $mockedNamespaces List of namespaces, indexed by mocked features (time-sensitive or dns-sensitive) @@ -121,7 +123,7 @@ public function startTestSuite($suite) $suiteName = $suite->getName(); foreach ($suite->tests() as $test) { - if (!($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) { + if (!$test instanceof TestCase) { continue; } if (null === Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) { @@ -156,7 +158,7 @@ public function startTestSuite($suite) $testSuites = [$suite]; for ($i = 0; isset($testSuites[$i]); ++$i) { foreach ($testSuites[$i]->tests() as $test) { - if ($test instanceof \PHPUnit_Framework_TestSuite || $test instanceof TestSuite) { + if ($test instanceof TestSuite) { if (!class_exists($test->getName(), false)) { $testSuites[] = $test; continue; @@ -176,11 +178,11 @@ public function startTestSuite($suite) $skipped = []; while ($s = array_shift($suites)) { foreach ($s->tests() as $test) { - if ($test instanceof \PHPUnit_Framework_TestSuite || $test instanceof TestSuite) { + if ($test instanceof TestSuite) { $suites[] = $test; continue; } - if (($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase) + if ($test instanceof TestCase && isset($this->wasSkipped[\get_class($test)][$test->getName()]) ) { $skipped[] = $test; @@ -200,11 +202,12 @@ public function addSkippedTest($test, \Exception $e, $time) public function startTest($test) { - if (-2 < $this->state && ($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) { + if (-2 < $this->state && $test instanceof TestCase) { // This event is triggered before the test is re-run in isolation if ($this->willBeIsolated($test)) { $this->runsInSeparateProcess = tempnam(sys_get_temp_dir(), 'deprec'); putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$this->runsInSeparateProcess); + putenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE='.tempnam(sys_get_temp_dir(), 'expectdeprec')); } $groups = Test::getGroups(\get_class($test), $test->getName(false)); @@ -228,21 +231,35 @@ public function startTest($test) if (isset($annotations['class']['expectedDeprecation'])) { $test->getTestResultObject()->addError($test, new AssertionFailedError('`@expectedDeprecation` annotations are not allowed at the class level.'), 0); } - if (isset($annotations['method']['expectedDeprecation'])) { - if (!\in_array('legacy', $groups, true)) { - $this->error = new AssertionFailedError('Only tests with the `@group legacy` annotation can have `@expectedDeprecation`.'); + if (isset($annotations['method']['expectedDeprecation']) || $this->checkNumAssertions = method_exists($test, 'expectDeprecation') && (new \ReflectionMethod($test, 'expectDeprecation'))->getFileName() === (new \ReflectionMethod(ExpectDeprecationTrait::class, 'expectDeprecation'))->getFileName()) { + if (isset($annotations['method']['expectedDeprecation'])) { + self::$expectedDeprecations = $annotations['method']['expectedDeprecation']; + self::$previousErrorHandler = set_error_handler([self::class, 'handleError']); + @trigger_error('Since symfony/phpunit-bridge 5.1: Using "@expectedDeprecation" annotations in tests is deprecated, use the "ExpectDeprecationTrait::expectDeprecation()" method instead.', \E_USER_DEPRECATED); } - $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); + if ($this->checkNumAssertions) { + $this->checkNumAssertions = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything(); + } - $this->expectedDeprecations = $annotations['method']['expectedDeprecation']; - $this->previousErrorHandler = set_error_handler([$this, 'handleError']); + $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); } } } public function endTest($test, $time) { + if ($file = getenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE')) { + putenv('SYMFONY_EXPECTED_DEPRECATIONS_SERIALIZE'); + $expectedDeprecations = file_get_contents($file); + if ($expectedDeprecations) { + self::$expectedDeprecations = array_merge(self::$expectedDeprecations, unserialize($expectedDeprecations)); + if (!self::$previousErrorHandler) { + self::$previousErrorHandler = set_error_handler([self::class, 'handleError']); + } + } + } + if (class_exists(DebugClassLoader::class, false)) { DebugClassLoader::checkClasses(); } @@ -250,9 +267,15 @@ public function endTest($test, $time) $className = \get_class($test); $groups = Test::getGroups($className, $test->getName(false)); - if ($errored = null !== $this->error) { - $test->getTestResultObject()->addError($test, $this->error, 0); - $this->error = null; + if ($this->checkNumAssertions) { + $assertions = \count(self::$expectedDeprecations) + $test->getNumAssertions(); + if ($test->doesNotPerformAssertions() && $assertions > 0) { + $test->getTestResultObject()->addFailure($test, new RiskyTestError(sprintf('This test is annotated with "@doesNotPerformAssertions", but performed %s assertions', $assertions)), $time); + } elseif ($assertions === 0 && $test->getTestResultObject()->noneSkipped()) { + $test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time); + } + + $this->checkNumAssertions = false; } if ($this->runsInSeparateProcess) { @@ -260,7 +283,7 @@ public function endTest($test, $time) unlink($this->runsInSeparateProcess); putenv('SYMFONY_DEPRECATIONS_SERIALIZE'); foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) { - $error = serialize(['deprecation' => $deprecation[1], 'class' => $className, 'method' => $test->getName(false), 'triggering_file' => isset($deprecation[2]) ? $deprecation[2] : null, 'files_stack' => isset($deprecation[3]) ? $deprecation[3] : []]); + $error = serialize(['deprecation' => $deprecation[1], 'class' => $className, 'method' => $test->getName(false), 'triggering_file' => $deprecation[2] ?? null, 'files_stack' => $deprecation[3] ?? []]); if ($deprecation[0]) { // unsilenced on purpose trigger_error($error, \E_USER_DEPRECATED); @@ -271,26 +294,28 @@ public function endTest($test, $time) $this->runsInSeparateProcess = false; } - if ($this->expectedDeprecations) { + if (self::$expectedDeprecations) { if (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true)) { - $test->addToAssertionCount(\count($this->expectedDeprecations)); + $test->addToAssertionCount(\count(self::$expectedDeprecations)); } restore_error_handler(); - if (!$errored && !\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) { + if (!\in_array('legacy', $groups, true)) { + $test->getTestResultObject()->addError($test, new AssertionFailedError('Only tests with the `@group legacy` annotation can expect a deprecation.'), 0); + } elseif (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) { try { $prefix = "@expectedDeprecation:\n"; - $test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", $this->expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", $this->gatheredDeprecations)."\n"); + $test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", self::$expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", self::$gatheredDeprecations)."\n"); } catch (AssertionFailedError $e) { $test->getTestResultObject()->addFailure($test, $e, $time); } } - $this->expectedDeprecations = $this->gatheredDeprecations = []; - $this->previousErrorHandler = null; + self::$expectedDeprecations = self::$gatheredDeprecations = []; + self::$previousErrorHandler = null; } - if (!$this->runsInSeparateProcess && -2 < $this->state && ($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) { + if (!$this->runsInSeparateProcess && -2 < $this->state && $test instanceof TestCase) { if (\in_array('time-sensitive', $groups, true)) { ClockMock::withClockMock(false); } @@ -300,10 +325,10 @@ public function endTest($test, $time) } } - public function handleError($type, $msg, $file, $line, $context = []) + public static function handleError($type, $msg, $file, $line, $context = []) { if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { - $h = $this->previousErrorHandler; + $h = self::$previousErrorHandler; return $h ? $h($type, $msg, $file, $line, $context) : false; } @@ -316,7 +341,7 @@ public function handleError($type, $msg, $file, $line, $context = []) if (error_reporting() & $type) { $msg = 'Unsilenced deprecation: '.$msg; } - $this->gatheredDeprecations[] = $msg; + self::$gatheredDeprecations[] = $msg; return null; } diff --git a/src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php b/src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php deleted file mode 100644 index e27c3a4fb0934..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit; - -use PHPUnit\Framework\TestCase; - -// A trait to provide forward compatibility with newest PHPUnit versions -$r = new \ReflectionClass(TestCase::class); -if (\PHP_VERSION_ID < 70000 || !$r->getMethod('setUp')->hasReturnType()) { - trait SetUpTearDownTrait - { - use Legacy\SetUpTearDownTraitForV5; - } -} else { - trait SetUpTearDownTrait - { - use Legacy\SetUpTearDownTraitForV8; - } -} diff --git a/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php b/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php index d3cd7563bd41f..47f0f42afc8fd 100644 --- a/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php +++ b/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php @@ -11,13 +11,7 @@ namespace Symfony\Bridge\PhpUnit; -if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { - class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV5', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); -} elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) { - class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); -} else { - class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); -} +class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); if (false) { class SymfonyTestsListener diff --git a/src/Symfony/Bridge/PhpUnit/Tests/BootstrapTest.php b/src/Symfony/Bridge/PhpUnit/Tests/BootstrapTest.php deleted file mode 100644 index d1811575087df..0000000000000 --- a/src/Symfony/Bridge/PhpUnit/Tests/BootstrapTest.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\PhpUnit\Tests; - -use PHPUnit\Framework\TestCase; - -class BootstrapTest extends TestCase -{ - /** - * @requires PHPUnit < 6.0 - */ - public function testAliasingOfErrorClasses() - { - $this->assertInstanceOf( - \PHPUnit_Framework_Error::class, - new \PHPUnit\Framework\Error\Error('message', 0, __FILE__, __LINE__) - ); - $this->assertInstanceOf( - \PHPUnit_Framework_Error_Deprecated::class, - new \PHPUnit\Framework\Error\Deprecated('message', 0, __FILE__, __LINE__) - ); - $this->assertInstanceOf( - \PHPUnit_Framework_Error_Notice::class, - new \PHPUnit\Framework\Error\Notice('message', 0, __FILE__, __LINE__) - ); - $this->assertInstanceOf( - \PHPUnit_Framework_Error_Warning::class, - new \PHPUnit\Framework\Error\Warning('message', 0, __FILE__, __LINE__) - ); - } -} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php index 53b2bb8d6cdff..b309606d5bd4e 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php @@ -14,7 +14,7 @@ public function test() exec('type phpdbg 2> /dev/null', $output, $returnCode); - if (\PHP_VERSION_ID >= 70000 && 0 === $returnCode) { + if (0 === $returnCode) { $php = 'phpdbg -qrr'; } else { exec('php --ri xdebug -d zend_extension=xdebug.so 2> /dev/null', $output, $returnCode); diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php index 39e792cd3a2cb..5d36a43bff54f 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -13,9 +13,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; class ConfigurationTest extends TestCase { + private $files; + public function testItThrowsOnStringishValue() { $this->expectException(\InvalidArgumentException::class); @@ -47,122 +51,122 @@ public function testItThrowsOnStringishThreshold() public function testItNoticesExceededTotalThreshold() { $configuration = Configuration::fromUrlEncodedString('max[total]=3'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1, - 'remaining selfCount' => 0, - 'legacyCount' => 1, - 'otherCount' => 0, - 'remaining directCount' => 1, - 'remaining indirectCount' => 1, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1, - 'remaining selfCount' => 1, - 'legacyCount' => 1, - 'otherCount' => 0, - 'remaining directCount' => 1, - 'remaining indirectCount' => 1, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1, + 'self' => 0, + 'legacy' => 1, + 'other' => 0, + 'direct' => 1, + 'indirect' => 1, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1, + 'self' => 1, + 'legacy' => 1, + 'other' => 0, + 'direct' => 1, + 'indirect' => 1, + ]))); } public function testItNoticesExceededSelfThreshold() { $configuration = Configuration::fromUrlEncodedString('max[self]=1'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 1, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 124, - 'remaining indirectCount' => 3244, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 2, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 124, - 'remaining indirectCount' => 3244, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 1, + 'legacy' => 23, + 'other' => 13, + 'direct' => 124, + 'indirect' => 3244, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 2, + 'legacy' => 23, + 'other' => 13, + 'direct' => 124, + 'indirect' => 3244, + ]))); } public function testItNoticesExceededDirectThreshold() { $configuration = Configuration::fromUrlEncodedString('max[direct]=1&max[self]=999999'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 123, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 1, - 'remaining indirectCount' => 3244, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 124, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 2, - 'remaining indirectCount' => 3244, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 123, + 'legacy' => 23, + 'other' => 13, + 'direct' => 1, + 'indirect' => 3244, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 124, + 'legacy' => 23, + 'other' => 13, + 'direct' => 2, + 'indirect' => 3244, + ]))); } public function testItNoticesExceededIndirectThreshold() { $configuration = Configuration::fromUrlEncodedString('max[indirect]=1&max[direct]=999999&max[self]=999999'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 123, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 1234, - 'remaining indirectCount' => 1, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 124, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 2324, - 'remaining indirectCount' => 2, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 123, + 'legacy' => 23, + 'other' => 13, + 'direct' => 1234, + 'indirect' => 1, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 124, + 'legacy' => 23, + 'other' => 13, + 'direct' => 2324, + 'indirect' => 2, + ]))); } public function testIndirectThresholdIsUsedAsADefaultForDirectAndSelfThreshold() { $configuration = Configuration::fromUrlEncodedString('max[indirect]=1'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 1, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 0, - 'remaining indirectCount' => 0, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 2, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 0, - 'remaining indirectCount' => 0, - ])); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 0, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 1, - 'remaining indirectCount' => 0, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 0, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 2, - 'remaining indirectCount' => 0, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 1, + 'legacy' => 0, + 'other' => 0, + 'direct' => 0, + 'indirect' => 0, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 2, + 'legacy' => 0, + 'other' => 0, + 'direct' => 0, + 'indirect' => 0, + ]))); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 0, + 'other' => 0, + 'direct' => 1, + 'indirect' => 0, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 0, + 'other' => 0, + 'direct' => 2, + 'indirect' => 0, + ]))); } public function testItCanTellWhetherToDisplayAStackTrace() @@ -175,21 +179,237 @@ public function testItCanTellWhetherToDisplayAStackTrace() $this->assertTrue($configuration->shouldDisplayStackTrace('interesting')); } - public function testItCanBeDisabled() + public function provideItCanBeDisabled(): array + { + return [ + ['disabled', false], + ['disabled=1', false], + ['disabled=0', true], + ]; + } + + /** + * @dataProvider provideItCanBeDisabled + */ + public function testItCanBeDisabled(string $encodedString, bool $expectedEnabled) { - $configuration = Configuration::fromUrlEncodedString('disabled'); - $this->assertFalse($configuration->isEnabled()); + $configuration = Configuration::fromUrlEncodedString($encodedString); + $this->assertSame($expectedEnabled, $configuration->isEnabled()); } public function testItCanBeShushed() { $configuration = Configuration::fromUrlEncodedString('verbose'); - $this->assertFalse($configuration->verboseOutput()); + $this->assertFalse($configuration->verboseOutput('unsilenced')); + $this->assertFalse($configuration->verboseOutput('direct')); + $this->assertFalse($configuration->verboseOutput('indirect')); + $this->assertFalse($configuration->verboseOutput('self')); + $this->assertFalse($configuration->verboseOutput('other')); + } + + public function testItCanBePartiallyShushed() + { + $configuration = Configuration::fromUrlEncodedString('quiet[]=unsilenced&quiet[]=indirect&quiet[]=other'); + $this->assertFalse($configuration->verboseOutput('unsilenced')); + $this->assertTrue($configuration->verboseOutput('direct')); + $this->assertFalse($configuration->verboseOutput('indirect')); + $this->assertTrue($configuration->verboseOutput('self')); + $this->assertFalse($configuration->verboseOutput('other')); + } + + public function testItThrowsOnUnknownVerbosityGroup() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('made-up'); + Configuration::fromUrlEncodedString('quiet[]=made-up'); } public function testOutputIsNotVerboseInWeakMode() { $configuration = Configuration::inWeakMode(); - $this->assertFalse($configuration->verboseOutput()); + $this->assertFalse($configuration->verboseOutput('unsilenced')); + $this->assertFalse($configuration->verboseOutput('direct')); + $this->assertFalse($configuration->verboseOutput('indirect')); + $this->assertFalse($configuration->verboseOutput('self')); + $this->assertFalse($configuration->verboseOutput('other')); + } + + private function buildGroups($counts) + { + $groups = []; + foreach ($counts as $name => $count) { + $groups[$name] = new DeprecationGroup(); + $i = 0; + while ($i++ < $count) { + $groups[$name]->addNotice(); + } + } + + return $groups; + } + + public function testBaselineGenerationEmptyFile() + { + $filename = $this->createFile(); + $configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile='.urlencode($filename)); + $this->assertTrue($configuration->isGeneratingBaseline()); + $trace = debug_backtrace(); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, ''))); + $configuration->writeBaseline(); + $this->assertEquals($filename, $configuration->getBaselineFile()); + $expected_baseline = [ + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 1', + 'count' => 2, + ], + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 2', + 'count' => 1, + ], + ]; + $this->assertEquals(json_encode($expected_baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename)); + } + + public function testBaselineGenerationNoFile() + { + $filename = $this->createFile(); + $configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile='.urlencode($filename)); + $this->assertTrue($configuration->isGeneratingBaseline()); + $trace = debug_backtrace(); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, ''))); + $configuration->writeBaseline(); + $this->assertEquals($filename, $configuration->getBaselineFile()); + $expected_baseline = [ + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 1', + 'count' => 2, + ], + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 2', + 'count' => 2, + ], + ]; + $this->assertEquals(json_encode($expected_baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename)); + } + + public function testExistingBaseline() + { + $filename = $this->createFile(); + $baseline = [ + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 1', + 'count' => 1, + ], + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 2', + 'count' => 1, + ], + ]; + file_put_contents($filename, json_encode($baseline)); + + $configuration = Configuration::fromUrlEncodedString('baselineFile='.urlencode($filename)); + $this->assertFalse($configuration->isGeneratingBaseline()); + $trace = debug_backtrace(); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 1', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, ''))); + $this->assertFalse($configuration->isBaselineDeprecation(new Deprecation('Test message 3', $trace, ''))); + $this->assertEquals($filename, $configuration->getBaselineFile()); + } + + public function testExistingBaselineAndGeneration() + { + $filename = $this->createFile(); + $baseline = [ + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 1', + 'count' => 1, + ], + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 2', + 'count' => 1, + ], + ]; + file_put_contents($filename, json_encode($baseline)); + $configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile='.urlencode($filename)); + $this->assertTrue($configuration->isGeneratingBaseline()); + $trace = debug_backtrace(); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 2', $trace, ''))); + $this->assertTrue($configuration->isBaselineDeprecation(new Deprecation('Test message 3', $trace, ''))); + $configuration->writeBaseline(); + $this->assertEquals($filename, $configuration->getBaselineFile()); + $expected_baseline = [ + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 2', + 'count' => 1, + ], + [ + 'location' => 'Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler\ConfigurationTest::runTest', + 'message' => 'Test message 3', + 'count' => 1, + ], + ]; + $this->assertEquals(json_encode($expected_baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES), file_get_contents($filename)); + } + + public function testBaselineArgumentException() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('You cannot use the "generateBaseline" configuration option without providing a "baselineFile" configuration option.'); + Configuration::fromUrlEncodedString('generateBaseline=true'); + } + + public function testBaselineFileException() + { + $filename = $this->createFile(); + unlink($filename); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('The baselineFile "%s" does not exist.', $filename)); + Configuration::fromUrlEncodedString('baselineFile='.urlencode($filename)); + } + + public function testBaselineFileWriteError() + { + $filename = $this->createFile(); + chmod($filename, 0444); + $this->expectError(); + $this->expectErrorMessageMatches('/[Ff]ailed to open stream: Permission denied/'); + $configuration = Configuration::fromUrlEncodedString('generateBaseline=true&baselineFile='.urlencode($filename)); + $configuration->writeBaseline(); + } + + protected function setUp(): void + { + $this->files = []; + } + + protected function tearDown(): void + { + foreach ($this->files as $file) { + if (file_exists($file)) { + @unlink($file); + } + } + } + + private function createFile() + { + $filename = tempnam(sys_get_temp_dir(), 'sf-'); + $this->files[] = $filename; + + return $filename; } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationGroupTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationGroupTest.php new file mode 100644 index 0000000000000..df746e5e38907 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationGroupTest.php @@ -0,0 +1,30 @@ +addNoticeFromObject( + 'Calling sfContext::getInstance() is deprecated', + 'MonsterController', + 'get5klocMethod' + ); + $group->addNoticeFromProceduralCode('Calling sfContext::getInstance() is deprecated'); + $this->assertCount(1, $group->notices()); + $this->assertSame(2, $group->count()); + } + + public function testItAllowsAddingANoticeWithoutClutteringTheMemory() + { + // this is useful for notices in the legacy group + $group = new DeprecationGroup(); + $group->addNotice(); + $this->assertSame(1, $group->count()); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php new file mode 100644 index 0000000000000..c0a88c443b4d7 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php @@ -0,0 +1,35 @@ +addObjectOccurrence('MyAction', '__invoke'); + $notice->addObjectOccurrence('MyAction', '__invoke'); + $notice->addObjectOccurrence('MyOtherAction', '__invoke'); + + $countsByCaller = $notice->getCountsByCaller(); + + $this->assertCount(2, $countsByCaller); + $this->assertArrayHasKey('MyAction::__invoke', $countsByCaller); + $this->assertArrayHasKey('MyOtherAction::__invoke', $countsByCaller); + $this->assertSame(2, $countsByCaller['MyAction::__invoke']); + $this->assertSame(1, $countsByCaller['MyOtherAction::__invoke']); + } + + public function testItCountsBothTypesOfOccurrences() + { + $notice = new DeprecationNotice(); + $notice->addObjectOccurrence('MyAction', '__invoke'); + $this->assertSame(1, $notice->count()); + + $notice->addProceduralOccurrence(); + $this->assertSame(2, $notice->count()); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php index 9cb0a0e32ce3a..a1d3c06ea668f 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php @@ -14,7 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; -use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV5; +use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7; class DeprecationTest extends TestCase { @@ -30,7 +30,7 @@ private static function getVendorDir() foreach (get_declared_classes() as $class) { if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); - $vendorDir = \dirname(\dirname($r->getFileName())); + $vendorDir = \dirname($r->getFileName(), 2); if (file_exists($vendorDir.'/composer/installed.json') && @mkdir($vendorDir.'/myfakevendor/myfakepackage1', 0777, true)) { break; } @@ -58,7 +58,7 @@ public function testItCanTellWhetherItIsInternal() { $r = new \ReflectionClass(Deprecation::class); - if (\dirname(\dirname($r->getFileName())) !== \dirname(\dirname(__DIR__))) { + if (\dirname($r->getFileName(), 2) !== \dirname(__DIR__, 2)) { $this->markTestSkipped('Test case is not compatible with having the bridge in vendor/'); } @@ -161,7 +161,7 @@ public function providerGetTypeDetectsSelf() 'triggering_file' => 'dummy_vendor_path', 'files_stack' => [], ]), - SymfonyTestsListenerForV5::class, + SymfonyTestsListenerForV7::class, '', ], ]; @@ -188,7 +188,7 @@ public function providerGetTypeUsesRightTrace() $fakeTrace = [ ['function' => 'trigger_error'], ['class' => SymfonyTestsListenerTrait::class, 'function' => 'endTest'], - ['class' => SymfonyTestsListenerForV5::class, 'function' => 'endTest'], + ['class' => SymfonyTestsListenerForV7::class, 'function' => 'endTest'], ]; return [ @@ -270,7 +270,7 @@ public static function setupBeforeClass(): void foreach (get_declared_classes() as $class) { if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); - $v = \dirname(\dirname($r->getFileName())); + $v = \dirname($r->getFileName(), 2); if (file_exists($v.'/composer/installed.json')) { $loader = require $v.'/autoload.php'; $reflection = new \ReflectionClass($loader); diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline.phpt new file mode 100644 index 0000000000000..533912c106cbd --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline.phpt @@ -0,0 +1,80 @@ +--TEST-- +Test DeprecationErrorHandler in baseline mode +--FILE-- + 'FooTestCase::testLegacyFoo', + 'message' => 'silenced foo deprecation', + 'count' => 1, +], +[ + 'location' => 'FooTestCase::testNonLegacyBar', + 'message' => 'silenced bar deprecation', + 'count' => 1, +], +[ + 'location' => 'procedural code', + 'message' => 'root deprecation', + 'count' => 1, +]]; +file_put_contents($filename, json_encode($baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + +$k = 'SYMFONY_DEPRECATIONS_HELPER'; +unset($_SERVER[$k], $_ENV[$k]); +putenv($k.'='.$_SERVER[$k] = $_ENV[$k] = 'baselineFile=' . urlencode($filename)); +putenv('ANSICON'); +putenv('ConEmuANSI'); +putenv('TERM'); + +$vendor = __DIR__; +while (!file_exists($vendor.'/vendor')) { + $vendor = dirname($vendor); +} +define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php'); +require PHPUNIT_COMPOSER_INSTALL; +require_once __DIR__.'/../../bootstrap.php'; + +@trigger_error('root deprecation', E_USER_DEPRECATED); + +eval(<<<'EOPHP' +namespace PHPUnit\Util; + +class Test +{ + public static function getGroups() + { + return array(); + } +} +EOPHP +); + +class PHPUnit_Util_Test +{ + public static function getGroups() + { + return array(); + } +} + +class FooTestCase +{ + public function testLegacyFoo() + { + @trigger_error('silenced foo deprecation', E_USER_DEPRECATED); + } + + public function testNonLegacyBar() + { + @trigger_error('silenced bar deprecation', E_USER_DEPRECATED); + } +} + +$foo = new FooTestCase(); +$foo->testLegacyFoo(); +$foo->testNonLegacyBar(); +print "Cannot test baselineFile contents because it is generated in a shutdown function registered by another shutdown function." +?> +--EXPECT-- +Cannot test baselineFile contents because it is generated in a shutdown function registered by another shutdown function. diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline2.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline2.phpt new file mode 100644 index 0000000000000..f520912694a1e --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline2.phpt @@ -0,0 +1,74 @@ +--TEST-- +Test DeprecationErrorHandler in baseline mode +--FILE-- + 'FooTestCase::testLegacyFoo', + 'message' => 'silenced foo deprecation', + 'count' => 1, +]]; +file_put_contents($filename, json_encode($baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + +$k = 'SYMFONY_DEPRECATIONS_HELPER'; +unset($_SERVER[$k], $_ENV[$k]); +putenv($k.'='.$_SERVER[$k] = $_ENV[$k] = 'baselineFile=' . urlencode($filename)); +putenv('ANSICON'); +putenv('ConEmuANSI'); +putenv('TERM'); + +$vendor = __DIR__; +while (!file_exists($vendor.'/vendor')) { + $vendor = dirname($vendor); +} +define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php'); +require PHPUNIT_COMPOSER_INSTALL; +require_once __DIR__.'/../../bootstrap.php'; + +@trigger_error('root deprecation', E_USER_DEPRECATED); + +eval(<<<'EOPHP' +namespace PHPUnit\Util; + +class Test +{ + public static function getGroups() + { + return array(); + } +} +EOPHP +); + +class PHPUnit_Util_Test +{ + public static function getGroups() + { + return array(); + } +} + +class FooTestCase +{ + public function testLegacyFoo() + { + @trigger_error('silenced foo deprecation', E_USER_DEPRECATED); + } + + public function testNonLegacyBar() + { + @trigger_error('silenced bar deprecation', E_USER_DEPRECATED); + } +} + +$foo = new FooTestCase(); +$foo->testLegacyFoo(); +$foo->testNonLegacyBar(); +?> +--EXPECTF-- +Other deprecation notices (2) + + 1x: root deprecation + + 1x: silenced bar deprecation + 1x in FooTestCase::testNonLegacyBar diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline3.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline3.phpt new file mode 100644 index 0000000000000..28d1a74ffd427 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/baseline3.phpt @@ -0,0 +1,79 @@ +--TEST-- +Test DeprecationErrorHandler in baseline mode +--FILE-- + 'FooTestCase::testLegacyFoo', + 'message' => 'silenced foo deprecation', + 'count' => 1, +]]; +file_put_contents($filename, json_encode($baseline, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + +$k = 'SYMFONY_DEPRECATIONS_HELPER'; +unset($_SERVER[$k], $_ENV[$k]); +putenv($k.'='.$_SERVER[$k] = $_ENV[$k] = 'baselineFile=' . urlencode($filename)); +putenv('ANSICON'); +putenv('ConEmuANSI'); +putenv('TERM'); + +$vendor = __DIR__; +while (!file_exists($vendor.'/vendor')) { + $vendor = dirname($vendor); +} +define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php'); +require PHPUNIT_COMPOSER_INSTALL; +require_once __DIR__.'/../../bootstrap.php'; + +@trigger_error('root deprecation', E_USER_DEPRECATED); + +eval(<<<'EOPHP' +namespace PHPUnit\Util; + +class Test +{ + public static function getGroups() + { + return array(); + } +} +EOPHP +); + +class PHPUnit_Util_Test +{ + public static function getGroups() + { + return array(); + } +} + +class FooTestCase +{ + public function testLegacyFoo() + { + @trigger_error('silenced foo deprecation', E_USER_DEPRECATED); + // This will cause a deprecation because the baseline only expects 1 + // deprecation. + @trigger_error('silenced foo deprecation', E_USER_DEPRECATED); + } + + public function testNonLegacyBar() + { + @trigger_error('silenced bar deprecation', E_USER_DEPRECATED); + } +} + +$foo = new FooTestCase(); +$foo->testLegacyFoo(); +$foo->testNonLegacyBar(); +?> +--EXPECTF-- +Legacy deprecation notices (1) + +Other deprecation notices (2) + + 1x: root deprecation + + 1x: silenced bar deprecation + 1x in FooTestCase::testNonLegacyBar diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/disabled_1.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/disabled_1.phpt new file mode 100644 index 0000000000000..acb5f096306d0 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/disabled_1.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test DeprecationErrorHandler in default mode +--FILE-- + +--EXPECTREGEX-- +.{0} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/AppService.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/AppService.php index 2e5ccf3c78811..2b6cb316af143 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/AppService.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/AppService.php @@ -7,6 +7,37 @@ final class AppService { + public function directDeprecationsTwoVendors() + { + $service1 = new SomeService(); + $service1->deprecatedApi(); + + $service2 = new SomeOtherService(); + $service2->deprecatedApi(); + } + + public function selfDeprecation(bool $useContracts = false) + { + $args = [__FUNCTION__, __FUNCTION__]; + if ($useContracts) { + trigger_deprecation('App', '3.0', sprintf('%s is deprecated, use %s_new instead.', ...$args)); + } else { + @trigger_error(sprintf('Since App 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); + } + } + + public function directDeprecation(bool $useContracts = false) + { + $service = new SomeService(); + $service->deprecatedApi($useContracts); + } + + public function indirectDeprecation(bool $useContracts = false) + { + $service = new SomeService(); + $service->indirectDeprecatedApi($useContracts); + } + public function directDeprecations() { $service1 = new SomeService(); diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php index d18cfbd3319bd..cc237e6146c23 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/acme/lib/SomeService.php @@ -2,13 +2,22 @@ namespace acme\lib; +use bar\lib\AnotherService; + class SomeService { - public function deprecatedApi() + public function deprecatedApi(bool $useContracts = false) + { + $args = [__FUNCTION__, __FUNCTION__]; + if ($useContracts) { + trigger_deprecation('acme/lib', '3.0', sprintf('%s is deprecated, use %s_new instead.', ...$args)); + } else { + @trigger_error(sprintf('Since acme/lib 3.0: %s is deprecated, use %s_new instead.', ...$args), \E_USER_DEPRECATED); + } + } + + public function indirectDeprecatedApi(bool $useContracts = false) { - @trigger_error( - __FUNCTION__.' is deprecated! You should stop relying on it!', - \E_USER_DEPRECATED - ); + (new AnotherService())->deprecatedApi($useContracts); } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php new file mode 100644 index 0000000000000..2e2f0f9b6b4b5 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/bar/lib/AnotherService.php @@ -0,0 +1,16 @@ + [__DIR__.'/../../fake_app/'], 'acme\\lib\\' => [__DIR__.'/../acme/lib/'], + 'bar\\lib\\' => [__DIR__.'/../bar/lib/'], 'fcy\\lib\\' => [__DIR__.'/../fcy/lib/'], ]; } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/generate_baseline.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/generate_baseline.phpt new file mode 100644 index 0000000000000..112a02b4c41a0 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/generate_baseline.phpt @@ -0,0 +1,64 @@ +--TEST-- +Test DeprecationErrorHandler in baseline generation mode +--FILE-- +testLegacyFoo(); +$foo->testNonLegacyBar(); +print "Cannot test baselineFile contents because it is generated in a shutdown function registered by another shutdown function." +?> +--EXPECT-- +Cannot test baselineFile contents because it is generated in a shutdown function registered by another shutdown function. diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/lagging_vendor.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/lagging_vendor.phpt index 2f7686fd98819..b5253df4299e4 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/lagging_vendor.phpt +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/lagging_vendor.phpt @@ -35,5 +35,5 @@ require __DIR__.'/fake_vendor/acme/outdated-lib/outdated_file.php'; --EXPECTF-- Remaining indirect deprecation notices (1) - 1x: deprecatedApi is deprecated! You should stop relying on it! + 1x: Since acme/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. 1x in SomeService::deprecatedApi from acme\lib diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt new file mode 100644 index 0000000000000..7f114ab5e2e5a --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/log_file.phpt @@ -0,0 +1,58 @@ +--TEST-- +Test DeprecationErrorHandler with log file +--FILE-- +testLegacyFoo(); +$foo->testLegacyBar(); + +register_shutdown_function(function () use ($filename) { + var_dump(file_get_contents($filename)); +}); +?> +--EXPECTF-- +string(234) " +Unsilenced deprecation notices (3) + + 2x: unsilenced foo deprecation + 2x in FooTestCase::testLegacyFoo + + 1x: unsilenced bar deprecation + 1x in FooTestCase::testLegacyBar + +Other deprecation notices (1) + + 1x: root deprecation + +" diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt index 336001f500113..edf9f4f6f3731 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt @@ -38,7 +38,7 @@ require __DIR__.'/fake_vendor_bis/autoload.php'; --EXPECTF-- Remaining direct deprecation notices (2) - 1x: deprecatedApi is deprecated! You should stop relying on it! + 1x: Since acme/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. 1x in AppService::directDeprecations from App\Services 1x: deprecatedApi from foo is deprecated! You should stop relying on it! diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet.phpt new file mode 100644 index 0000000000000..83b135b34bab7 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet.phpt @@ -0,0 +1,35 @@ +--TEST-- +Test DeprecationErrorHandler quiet on everything but indirect deprecations +--FILE-- + +--EXPECTF-- +Unsilenced deprecation notices (3) + +Remaining direct deprecation notices (2) + +Remaining indirect deprecation notices (1) + + 1x: Since acme/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. + 1x in SomeService::deprecatedApi from acme\lib + +Legacy deprecation notices (2) + diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet_but_failing.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet_but_failing.phpt new file mode 100644 index 0000000000000..8d8f8b4ff490d --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet_but_failing.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test DeprecationErrorHandler when failing and not verbose +--FILE-- + +--EXPECTF-- +Remaining indirect deprecation notices (1) + + 1x: Since acme/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. + 1x in SomeService::deprecatedApi from acme\lib diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/trigger_deprecation_types.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/trigger_deprecation_types.phpt new file mode 100644 index 0000000000000..261b6ec83f675 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/trigger_deprecation_types.phpt @@ -0,0 +1,58 @@ +--TEST-- +Test deprecation types with trigger_deprecation +--FILE-- +selfDeprecation(true); +(new \App\Services\AppService())->directDeprecation(true); +(new \App\Services\AppService())->indirectDeprecation(true); +trigger_deprecation('foo/bar', '2.0', 'func is deprecated, use new instead.'); +?> +--EXPECTF-- +Remaining self deprecation notices (1) + + 1x: Since App 3.0: selfDeprecation is deprecated, use selfDeprecation_new instead. + 1x in AppService::selfDeprecation from App\Services + +Remaining direct deprecation notices (1) + + 1x: Since acme/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. + 1x in AppService::directDeprecation from App\Services + +Remaining indirect deprecation notices (1) + + 1x: Since bar/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. + 1x in AppService::indirectDeprecation from App\Services + +Other deprecation notices (1) + + 1x: Since foo/bar 2.0: func is deprecated, use new instead. diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/trigger_error_types.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/trigger_error_types.phpt new file mode 100644 index 0000000000000..4dd6bdaed9f0b --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/trigger_error_types.phpt @@ -0,0 +1,58 @@ +--TEST-- +Test deprecation types with trigger_error +--FILE-- +selfDeprecation(); +(new \App\Services\AppService())->directDeprecation(); +(new \App\Services\AppService())->indirectDeprecation(); +@trigger_error('Since foo/bar 2.0: func is deprecated, use new instead.', E_USER_DEPRECATED); +?> +--EXPECTF-- +Remaining self deprecation notices (1) + + 1x: Since App 3.0: selfDeprecation is deprecated, use selfDeprecation_new instead. + 1x in AppService::selfDeprecation from App\Services + +Remaining direct deprecation notices (1) + + 1x: Since acme/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. + 1x in AppService::directDeprecation from App\Services + +Remaining indirect deprecation notices (1) + + 1x: Since bar/lib 3.0: deprecatedApi is deprecated, use deprecatedApi_new instead. + 1x in AppService::indirectDeprecation from App\Services + +Other deprecation notices (1) + + 1x: Since foo/bar 2.0: func is deprecated, use new instead. diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ExpectDeprecationTraitTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ExpectDeprecationTraitTest.php new file mode 100644 index 0000000000000..5e6350e673101 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/ExpectDeprecationTraitTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + +final class ExpectDeprecationTraitTest extends TestCase +{ + use ExpectDeprecationTrait; + + /** + * Do not remove this test in the next major version. + * + * @group legacy + */ + public function testOne() + { + $this->expectDeprecation('foo'); + @trigger_error('foo', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + * @runInSeparateProcess + */ + public function testOneInIsolation() + { + $this->expectDeprecation('foo'); + @trigger_error('foo', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + */ + public function testMany() + { + $this->expectDeprecation('foo'); + $this->expectDeprecation('bar'); + @trigger_error('foo', \E_USER_DEPRECATED); + @trigger_error('bar', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + * + * @expectedDeprecation foo + */ + public function testOneWithAnnotation() + { + $this->expectDeprecation('bar'); + @trigger_error('foo', \E_USER_DEPRECATED); + @trigger_error('bar', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + * + * @expectedDeprecation foo + * @expectedDeprecation bar + */ + public function testManyWithAnnotation() + { + $this->expectDeprecation('ccc'); + $this->expectDeprecation('fcy'); + @trigger_error('foo', \E_USER_DEPRECATED); + @trigger_error('bar', \E_USER_DEPRECATED); + @trigger_error('ccc', \E_USER_DEPRECATED); + @trigger_error('fcy', \E_USER_DEPRECATED); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/FailTests/ExpectDeprecationTraitTestFail.php b/src/Symfony/Bridge/PhpUnit/Tests/FailTests/ExpectDeprecationTraitTestFail.php new file mode 100644 index 0000000000000..ba35b268deebe --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/FailTests/ExpectDeprecationTraitTestFail.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests\FailTests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + +/** + * Class ExpectDeprecationTraitTestFail. + * + * This class is deliberately suffixed with *TestFail.php so that it is ignored + * by PHPUnit. This test is designed to fail. See ../expectdeprecationfail.phpt. + */ +final class ExpectDeprecationTraitTestFail extends TestCase +{ + use ExpectDeprecationTrait; + + /** + * Do not remove this test in the next major version. + * + * @group legacy + */ + public function testOne() + { + $this->expectDeprecation('foo'); + @trigger_error('bar', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + * @runInSeparateProcess + */ + public function testOneInIsolation() + { + $this->expectDeprecation('foo'); + @trigger_error('bar', \E_USER_DEPRECATED); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/FailTests/NoAssertionsTestRisky.php b/src/Symfony/Bridge/PhpUnit/Tests/FailTests/NoAssertionsTestRisky.php new file mode 100644 index 0000000000000..4a22baf125a7f --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/FailTests/NoAssertionsTestRisky.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests\FailTests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + +/** + * This class is deliberately suffixed with *TestRisky.php so that it is ignored + * by PHPUnit. This test is designed to fail. See ../expectrisky.phpt. + */ +final class NoAssertionsTestRisky extends TestCase +{ + use ExpectDeprecationTrait; + + /** + * Do not remove this test in the next major version. + * + * @group legacy + */ + public function testOne() + { + $this->expectNotToPerformAssertions(); + $this->expectDeprecation('foo'); + @trigger_error('foo', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + */ + public function testTwo() + { + $this->expectNotToPerformAssertions(); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php index 3e45381dce2a0..e302fa05ea74c 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php @@ -12,14 +12,4 @@ require __DIR__.'/../src/BarCov.php'; require __DIR__.'/../src/FooCov.php'; -require __DIR__.'/../../../../Legacy/CoverageListenerTrait.php'; - -if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { - require_once __DIR__.'/../../../../Legacy/CoverageListenerForV5.php'; -} elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) { - require_once __DIR__.'/../../../../Legacy/CoverageListenerForV6.php'; -} else { - require_once __DIR__.'/../../../../Legacy/CoverageListenerForV7.php'; -} - require __DIR__.'/../../../../CoverageListener.php'; diff --git a/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt b/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt new file mode 100644 index 0000000000000..f968cd188a0a7 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test ExpectDeprecationTrait failing tests +--FILE-- + +--EXPECTF-- +PHPUnit %s + +%ATesting Symfony\Bridge\PhpUnit\Tests\FailTests\ExpectDeprecationTraitTestFail +FF 2 / 2 (100%) + +Time: %s, Memory: %s + +There were 2 failures: + +1) Symfony\Bridge\PhpUnit\Tests\FailTests\ExpectDeprecationTraitTestFail::testOne +Failed asserting that string matches format description. +--- Expected ++++ Actual +@@ @@ + @expectedDeprecation: +-%A foo ++ bar + +2) Symfony\Bridge\PhpUnit\Tests\FailTests\ExpectDeprecationTraitTestFail::testOneInIsolation +Failed asserting that string matches format description. +--- Expected ++++ Actual +@@ @@ + @expectedDeprecation: +-%A foo ++ bar + +FAILURES! +Tests: 2, Assertions: 2, Failures: 2. diff --git a/src/Symfony/Bridge/PhpUnit/Tests/expectrisky.phpt b/src/Symfony/Bridge/PhpUnit/Tests/expectrisky.phpt new file mode 100644 index 0000000000000..91e0830553950 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/expectrisky.phpt @@ -0,0 +1,24 @@ +--TEST-- +Test NoAssertionsTestRisky risky test +--SKIPIF-- + +--EXPECTF-- +PHPUnit %s + +%ATesting Symfony\Bridge\PhpUnit\Tests\FailTests\NoAssertionsTestRisky +R. 2 / 2 (100%) + +Time: %s, Memory: %s + +There was 1 risky test: + +1) Symfony\Bridge\PhpUnit\Tests\FailTests\NoAssertionsTestRisky::testOne +This test is annotated with "@doesNotPerformAssertions", but performed 1 assertions + +OK, but incomplete, skipped, or risky tests! +Tests: 2, Assertions: 1, Risky: 1. diff --git a/src/Symfony/Bridge/PhpUnit/TextUI/Command.php b/src/Symfony/Bridge/PhpUnit/TextUI/Command.php index 8690812b56b57..3cc158f6b8e72 100644 --- a/src/Symfony/Bridge/PhpUnit/TextUI/Command.php +++ b/src/Symfony/Bridge/PhpUnit/TextUI/Command.php @@ -11,10 +11,8 @@ namespace Symfony\Bridge\PhpUnit\TextUI; -if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { - class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV5', 'Symfony\Bridge\PhpUnit\TextUI\Command'); -} elseif (version_compare(\PHPUnit\Runner\Version::id(), '9.0.0', '<')) { - class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV6', 'Symfony\Bridge\PhpUnit\TextUI\Command'); +if (version_compare(\PHPUnit\Runner\Version::id(), '9.0.0', '<')) { + class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV7', 'Symfony\Bridge\PhpUnit\TextUI\Command'); } else { class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV9', 'Symfony\Bridge\PhpUnit\TextUI\Command'); } diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index e7c3da2b8d7a3..ea88872ae9b86 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -15,8 +15,8 @@ error_reporting(-1); global $argv, $argc; -$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : []; -$argc = isset($_SERVER['argc']) ? $_SERVER['argc'] : 0; +$argv = $_SERVER['argv'] ?? []; +$argc = $_SERVER['argc'] ?? 0; $getEnvVar = function ($name, $default = false) use ($argv) { if (false !== $value = getenv($name)) { return $value; @@ -99,18 +99,14 @@ } elseif (\PHP_VERSION_ID >= 70200) { // PHPUnit 8 requires PHP 7.2+ $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.5') ?: '8.5'; -} elseif (\PHP_VERSION_ID >= 70100) { - // PHPUnit 7 requires PHP 7.1+ - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '7.5') ?: '7.5'; -} elseif (\PHP_VERSION_ID >= 70000) { - // PHPUnit 6 requires PHP 7.0+ - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '6.5') ?: '6.5'; -} elseif (\PHP_VERSION_ID >= 50600) { - // PHPUnit 4 does not support PHP 7 - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '5.7') ?: '5.7'; } else { - // PHPUnit 5.1 requires PHP 5.6+ - $PHPUNIT_VERSION = '4.8'; + $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '7.5') ?: '7.5'; +} + +$MAX_PHPUNIT_VERSION = $getEnvVar('SYMFONY_MAX_PHPUNIT_VERSION', false); + +if ($MAX_PHPUNIT_VERSION && version_compare($MAX_PHPUNIT_VERSION, $PHPUNIT_VERSION, '<')) { + $PHPUNIT_VERSION = $MAX_PHPUNIT_VERSION; } $PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN); @@ -137,6 +133,7 @@ 'COMPOSER' => 'composer.json', 'COMPOSER_VENDOR_DIR' => 'vendor', 'COMPOSER_BIN_DIR' => 'bin', + 'SYMFONY_SIMPLE_PHPUNIT_BIN_DIR' => __DIR__, ]; foreach ($defaultEnvs as $envName => $envValue) { @@ -171,7 +168,8 @@ } } $SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml' : '')); -$configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT])); +$SYMFONY_PHPUNIT_REQUIRE = $getEnvVar('SYMFONY_PHPUNIT_REQUIRE', ''); +$configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, $SYMFONY_PHPUNIT_REQUIRE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT])); $PHPUNIT_VERSION_DIR = sprintf('phpunit-%s-%d', $PHPUNIT_VERSION, $PHPUNIT_REMOVE_RETURN_TYPEHINT); if (!file_exists("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit") || $configurationHash !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION_DIR.md5")) { // Build a standalone phpunit without symfony/yaml nor prophecy by default @@ -228,6 +226,9 @@ if ($SYMFONY_PHPUNIT_REMOVE) { $passthruOrFail("$COMPOSER remove --no-update --no-interaction ".$SYMFONY_PHPUNIT_REMOVE); } + if ($SYMFONY_PHPUNIT_REQUIRE) { + $passthruOrFail("$COMPOSER require --no-update --no-interaction ".$SYMFONY_PHPUNIT_REQUIRE); + } if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { $passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); } @@ -271,12 +272,12 @@ if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) { $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode); } - $alteredCode = preg_replace('/abstract class (?:TestCase|PHPUnit_Framework_TestCase)[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;", $alteredCode, 1); + $alteredCode = preg_replace('/abstract class TestCase[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;", $alteredCode, 1); file_put_contents($alteredFile, $alteredCode); // Mutate Assert code $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php'); - $alteredCode = preg_replace('/abstract class (?:Assert|PHPUnit_Framework_Assert)[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1); + $alteredCode = preg_replace('/abstract class Assert[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1); file_put_contents($alteredFile, $alteredCode); file_put_contents('phpunit', <<<'EOPHP' @@ -373,7 +374,7 @@ class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Bla } if ($components) { - $skippedTests = isset($_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS']) ? $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] : false; + $skippedTests = $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] ?? false; $runningProcs = []; foreach ($components as $component) { diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php index d9947a7f4e1c8..e07c8d6cf5de8 100644 --- a/src/Symfony/Bridge/PhpUnit/bootstrap.php +++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php @@ -12,96 +12,6 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; -if (class_exists(\PHPUnit_Runner_Version::class) && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { - $classes = [ - 'PHPUnit_Framework_Assert', // override PhpUnit's ForwardCompat child class - 'PHPUnit_Framework_AssertionFailedError', // override PhpUnit's ForwardCompat child class - 'PHPUnit_Framework_BaseTestListener', // override PhpUnit's ForwardCompat child class - - 'PHPUnit_Framework_Constraint', - 'PHPUnit_Framework_Constraint_ArrayHasKey', - 'PHPUnit_Framework_Constraint_ArraySubset', - 'PHPUnit_Framework_Constraint_Attribute', - 'PHPUnit_Framework_Constraint_Callback', - 'PHPUnit_Framework_Constraint_ClassHasAttribute', - 'PHPUnit_Framework_Constraint_ClassHasStaticAttribute', - 'PHPUnit_Framework_Constraint_Composite', - 'PHPUnit_Framework_Constraint_Count', - 'PHPUnit_Framework_Constraint_Exception', - 'PHPUnit_Framework_Constraint_ExceptionCode', - 'PHPUnit_Framework_Constraint_ExceptionMessage', - 'PHPUnit_Framework_Constraint_ExceptionMessageRegExp', - 'PHPUnit_Framework_Constraint_FileExists', - 'PHPUnit_Framework_Constraint_GreaterThan', - 'PHPUnit_Framework_Constraint_IsAnything', - 'PHPUnit_Framework_Constraint_IsEmpty', - 'PHPUnit_Framework_Constraint_IsEqual', - 'PHPUnit_Framework_Constraint_IsFalse', - 'PHPUnit_Framework_Constraint_IsIdentical', - 'PHPUnit_Framework_Constraint_IsInstanceOf', - 'PHPUnit_Framework_Constraint_IsJson', - 'PHPUnit_Framework_Constraint_IsNull', - 'PHPUnit_Framework_Constraint_IsTrue', - 'PHPUnit_Framework_Constraint_IsType', - 'PHPUnit_Framework_Constraint_JsonMatches', - 'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider', - 'PHPUnit_Framework_Constraint_LessThan', - 'PHPUnit_Framework_Constraint_ObjectHasAttribute', - 'PHPUnit_Framework_Constraint_PCREMatch', - 'PHPUnit_Framework_Constraint_SameSize', - 'PHPUnit_Framework_Constraint_StringContains', - 'PHPUnit_Framework_Constraint_StringEndsWith', - 'PHPUnit_Framework_Constraint_StringMatches', - 'PHPUnit_Framework_Constraint_StringStartsWith', - 'PHPUnit_Framework_Constraint_TraversableContains', - 'PHPUnit_Framework_Constraint_TraversableContainsOnly', - - 'PHPUnit_Framework_Error_Deprecated', - 'PHPUnit_Framework_Error_Notice', - 'PHPUnit_Framework_Error_Warning', - 'PHPUnit_Framework_Exception', - 'PHPUnit_Framework_ExpectationFailedException', - - 'PHPUnit_Framework_MockObject_MockObject', - - 'PHPUnit_Framework_IncompleteTest', - 'PHPUnit_Framework_IncompleteTestCase', - 'PHPUnit_Framework_IncompleteTestError', - 'PHPUnit_Framework_RiskyTest', - 'PHPUnit_Framework_RiskyTestError', - 'PHPUnit_Framework_SkippedTest', - 'PHPUnit_Framework_SkippedTestCase', - 'PHPUnit_Framework_SkippedTestError', - 'PHPUnit_Framework_SkippedTestSuiteError', - - 'PHPUnit_Framework_SyntheticError', - - 'PHPUnit_Framework_Test', - 'PHPUnit_Framework_TestCase', // override PhpUnit's ForwardCompat child class - 'PHPUnit_Framework_TestFailure', - 'PHPUnit_Framework_TestListener', - 'PHPUnit_Framework_TestResult', - 'PHPUnit_Framework_TestSuite', // override PhpUnit's ForwardCompat child class - - 'PHPUnit_Runner_BaseTestRunner', - 'PHPUnit_Runner_Version', - - 'PHPUnit_Util_Blacklist', - 'PHPUnit_Util_ErrorHandler', - 'PHPUnit_Util_Test', - 'PHPUnit_Util_XML', - ]; - foreach ($classes as $class) { - class_alias($class, '\\'.strtr($class, '_', '\\')); - } - - class_alias('PHPUnit_Framework_Constraint_And', 'PHPUnit\Framework\Constraint\LogicalAnd'); - class_alias('PHPUnit_Framework_Constraint_Not', 'PHPUnit\Framework\Constraint\LogicalNot'); - class_alias('PHPUnit_Framework_Constraint_Or', 'PHPUnit\Framework\Constraint\LogicalOr'); - class_alias('PHPUnit_Framework_Constraint_Xor', 'PHPUnit\Framework\Constraint\LogicalXor'); - class_alias('PHPUnit_Framework_Error', 'PHPUnit\Framework\Error\Error'); -} - // Detect if we need to serialize deprecations to a file. if ($file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) { DeprecationErrorHandler::collectDeprecations($file); @@ -110,7 +20,7 @@ class_alias('PHPUnit_Framework_Error', 'PHPUnit\Framework\Error\Error'); } // Detect if we're loaded by an actual run of phpunit -if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists(\PHPUnit_TextUI_Command::class, false) && !class_exists(\PHPUnit\TextUI\Command::class, false)) { +if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists(\PHPUnit\TextUI\Command::class, false)) { return; } diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 54cb26cf70164..b0ccda04315f1 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -16,18 +16,19 @@ } ], "require": { - "php": ">=5.5.9 EVEN ON LATEST SYMFONY VERSIONS TO ALLOW USING", + "php": ">=7.1.3 EVEN ON LATEST SYMFONY VERSIONS TO ALLOW USING", "php": "THIS BRIDGE WHEN TESTING LOWEST SYMFONY VERSIONS.", - "php": ">=5.5.9" + "php": ">=7.1.3" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0" + "symfony/deprecation-contracts": "^2.1|^3.0", + "symfony/error-handler": "^5.4|^6.0" }, "suggest": { "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0|9.1.2" + "phpunit/phpunit": "<7.5|9.1.2" }, "autoload": { "files": [ "bootstrap.php" ], diff --git a/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist b/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist index d37d2eac3650a..cde576e2c7536 100644 --- a/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist +++ b/src/Symfony/Bridge/PhpUnit/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Tests - ./vendor - - - + + + ./Tests + ./vendor + + diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactory.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactory.php index caea93ac5d9f4..abd2b1d384f1d 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactory.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/LazyLoadingValueHolderFactory.php @@ -27,6 +27,6 @@ class LazyLoadingValueHolderFactory extends BaseFactory */ public function getGenerator(): ProxyGeneratorInterface { - return $this->generator ?: $this->generator = new LazyLoadingValueHolderGenerator(); + return $this->generator ??= new LazyLoadingValueHolderGenerator(); } } diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php index 7f5aa8ab51d86..1fd3459ae4cb4 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php @@ -38,7 +38,7 @@ public function __construct() /** * {@inheritdoc} */ - public function instantiateProxy(ContainerInterface $container, Definition $definition, $id, $realInstantiator) + public function instantiateProxy(ContainerInterface $container, Definition $definition, string $id, callable $realInstantiator): object { return $this->factory->createProxy( $this->factory->getGenerator()->getProxifiedClass($definition), diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php index 599fa7c6ec404..d3f23d393e50f 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php @@ -25,7 +25,7 @@ */ class ProxyDumper implements DumperInterface { - private $salt; + private string $salt; private $proxyGenerator; private $classGenerator; @@ -47,7 +47,7 @@ public function isProxyCandidate(Definition $definition): bool /** * {@inheritdoc} */ - public function getProxyFactoryCode(Definition $definition, $id, $factoryCode = null): string + public function getProxyFactoryCode(Definition $definition, string $id, string $factoryCode): string { $instantiation = 'return'; @@ -55,10 +55,6 @@ public function getProxyFactoryCode(Definition $definition, $id, $factoryCode = $instantiation .= sprintf(' $this->%s[%s] =', $definition->isPublic() && !$definition->isPrivate() ? 'services' : 'privates', var_export($id, true)); } - if (null === $factoryCode) { - throw new \InvalidArgumentException(sprintf('Missing factory code to construct the service "%s".', $id)); - } - $proxyClass = $this->getProxyClassName($definition); return <<expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Missing factory code to construct the service "foo".'); - $definition = new Definition(__CLASS__); - $definition->setLazy(true); - $this->dumper->getProxyFactoryCode($definition, 'foo'); - } - public function testGetProxyFactoryCodeForInterface() { $class = DummyClass::class; diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 577138489e690..430c4edd1e608 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -16,13 +16,12 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=8.0.2", "friendsofphp/proxy-manager-lts": "^1.0.2", - "symfony/dependency-injection": "^4.0|^5.0", - "symfony/polyfill-php80": "^1.16" + "symfony/dependency-injection": "^5.4|^6.0" }, "require-dev": { - "symfony/config": "^3.4|^4.0|^5.0" + "symfony/config": "^5.4|^6.0" }, "autoload": { "psr-4": { "Symfony\\Bridge\\ProxyManager\\": "" }, diff --git a/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist b/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist index 60d6ffc3aab3e..d93048d2cbe15 100644 --- a/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist +++ b/src/Symfony/Bridge/ProxyManager/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bridge/Twig/AppVariable.php b/src/Symfony/Bridge/Twig/AppVariable.php index 4a22265908b42..a6103a25c884a 100644 --- a/src/Symfony/Bridge/Twig/AppVariable.php +++ b/src/Symfony/Bridge/Twig/AppVariable.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * Exposes some Symfony parameters and services as an "app" global variable. @@ -26,8 +27,8 @@ class AppVariable { private $tokenStorage; private $requestStack; - private $environment; - private $debug; + private string $environment; + private bool $debug; public function setTokenStorage(TokenStorageInterface $tokenStorage) { @@ -39,62 +40,50 @@ public function setRequestStack(RequestStack $requestStack) $this->requestStack = $requestStack; } - public function setEnvironment($environment) + public function setEnvironment(string $environment) { $this->environment = $environment; } - public function setDebug($debug) + public function setDebug(bool $debug) { - $this->debug = (bool) $debug; + $this->debug = $debug; } /** * Returns the current token. * - * @return TokenInterface|null - * * @throws \RuntimeException When the TokenStorage is not available */ - public function getToken() + public function getToken(): ?TokenInterface { - if (null === $tokenStorage = $this->tokenStorage) { + if (!isset($this->tokenStorage)) { throw new \RuntimeException('The "app.token" variable is not available.'); } - return $tokenStorage->getToken(); + return $this->tokenStorage->getToken(); } /** * Returns the current user. * - * @return object|null - * * @see TokenInterface::getUser() */ - public function getUser() + public function getUser(): ?UserInterface { - if (null === $tokenStorage = $this->tokenStorage) { + if (!isset($this->tokenStorage)) { throw new \RuntimeException('The "app.user" variable is not available.'); } - if (!$token = $tokenStorage->getToken()) { - return null; - } - - $user = $token->getUser(); - - return \is_object($user) ? $user : null; + return $this->tokenStorage->getToken()?->getUser(); } /** * Returns the current request. - * - * @return Request|null The HTTP request object */ - public function getRequest() + public function getRequest(): ?Request { - if (null === $this->requestStack) { + if (!isset($this->requestStack)) { throw new \RuntimeException('The "app.request" variable is not available.'); } @@ -103,12 +92,10 @@ public function getRequest() /** * Returns the current session. - * - * @return Session|null The session */ - public function getSession() + public function getSession(): ?Session { - if (null === $this->requestStack) { + if (!isset($this->requestStack)) { throw new \RuntimeException('The "app.session" variable is not available.'); } $request = $this->getRequest(); @@ -118,12 +105,10 @@ public function getSession() /** * Returns the current app environment. - * - * @return string The current environment string (e.g 'dev') */ - public function getEnvironment() + public function getEnvironment(): string { - if (null === $this->environment) { + if (!isset($this->environment)) { throw new \RuntimeException('The "app.environment" variable is not available.'); } @@ -132,12 +117,10 @@ public function getEnvironment() /** * Returns the current app debug mode. - * - * @return bool The current debug mode */ - public function getDebug() + public function getDebug(): bool { - if (null === $this->debug) { + if (!isset($this->debug)) { throw new \RuntimeException('The "app.debug" variable is not available.'); } @@ -149,10 +132,8 @@ public function getDebug() * * getFlashes() returns all the flash messages * * getFlashes('notice') returns a simple array with flash messages of that type * * getFlashes(['notice', 'error']) returns a nested array of type => messages. - * - * @return array */ - public function getFlashes($types = null) + public function getFlashes(string|array $types = null): array { try { if (null === $session = $this->getSession()) { diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 71a30ae880d78..535df0c0897b4 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -1,12 +1,47 @@ CHANGELOG ========= +5.4 +--- + +* Add `github` format & autodetection to render errors as annotations when + running the Twig linter command in a Github Actions environment. + +5.3 +--- + + * Add a new `markAsPublic` method on `NotificationEmail` to change the `importance` context option to null after creation + * Add a new `fragment_uri()` helper to generate the URI of a fragment + * Add support of Bootstrap 5 for form theming + * Add a new `serialize` filter to serialize objects using the Serializer component + +5.2.0 +----- + + * added the `impersonation_exit_url()` and `impersonation_exit_path()` functions. They return a URL that allows to switch back to the original user. + * added the `workflow_transition()` function to easily retrieve a specific transition object + * added support for translating `TranslatableInterface` objects + * added the `t()` function to easily create `TranslatableMessage` objects + * Added support for extracting messages from the `t()` function + * Added `field_*` Twig functions to access string values from Form fields + * changed the `importance` context option of `NotificationEmail` to allow `null` + +5.0.0 +----- + + * removed `TwigEngine` class, use `\Twig\Environment` instead. + * removed `transChoice` filter and token + * `HttpFoundationExtension` requires a `UrlHelper` on instantiation + * removed support for implicit STDIN usage in the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. + * added form theme for Foundation 6 + * added support for Foundation 6 switches: add the `switch-input` class to the attributes of a `CheckboxType` + 4.4.0 ----- * added a new `TwigErrorRenderer` for `html` format, integrated with the `ErrorHandler` component * marked all classes extending twig as `@final` - * deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the + * deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the `DebugCommand::__construct()` method, swap the variables position. * the `LintCommand` lints all the templates stored in all configured Twig paths if none argument is provided * deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. @@ -19,7 +54,7 @@ CHANGELOG * added the `form_parent()` function that allows to reliably retrieve the parent form in Twig templates * added the `workflow_transition_blockers()` function - * deprecated the `$requestStack` and `$requestContext` arguments of the + * deprecated the `$requestStack` and `$requestContext` arguments of the `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` instance as the only argument instead diff --git a/src/Symfony/Bridge/Twig/Command/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php index a6a1e9a2cebde..e78723be91d1f 100644 --- a/src/Symfony/Bridge/Twig/Command/DebugCommand.php +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -11,7 +11,10 @@ namespace Symfony\Bridge\Twig\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputArgument; @@ -30,23 +33,22 @@ * * @author Jordi Boggiano */ +#[AsCommand(name: 'debug:twig', description: 'Show a list of twig functions, filters, globals and tests')] class DebugCommand extends Command { - protected static $defaultName = 'debug:twig'; - private $twig; - private $projectDir; - private $bundlesMetadata; - private $twigDefaultPath; - private $rootDir; - private $filesystemLoaders; - private $fileLinkFormatter; + private ?string $projectDir; + private array $bundlesMetadata; + private ?string $twigDefaultPath; /** - * @param FileLinkFormatter|null $fileLinkFormatter - * @param string|null $rootDir + * @var FilesystemLoader[] */ - public function __construct(Environment $twig, string $projectDir = null, array $bundlesMetadata = [], string $twigDefaultPath = null, $fileLinkFormatter = null, $rootDir = null) + private array $filesystemLoaders; + + private $fileLinkFormatter; + + public function __construct(Environment $twig, string $projectDir = null, array $bundlesMetadata = [], string $twigDefaultPath = null, FileLinkFormatter $fileLinkFormatter = null) { parent::__construct(); @@ -54,16 +56,7 @@ public function __construct(Environment $twig, string $projectDir = null, array $this->projectDir = $projectDir; $this->bundlesMetadata = $bundlesMetadata; $this->twigDefaultPath = $twigDefaultPath; - - if (\is_string($fileLinkFormatter) || $rootDir instanceof FileLinkFormatter) { - @trigger_error(sprintf('Passing a string as "$fileLinkFormatter" 5th argument or an instance of FileLinkFormatter as "$rootDir" 6th argument of the "%s()" method is deprecated since Symfony 4.4, swap the variables position.', __METHOD__), \E_USER_DEPRECATED); - - $this->rootDir = $fileLinkFormatter; - $this->fileLinkFormatter = $rootDir; - } else { - $this->fileLinkFormatter = $fileLinkFormatter; - $this->rootDir = $rootDir; - } + $this->fileLinkFormatter = $fileLinkFormatter; } protected function configure() @@ -74,7 +67,6 @@ protected function configure() new InputOption('filter', null, InputOption::VALUE_REQUIRED, 'Show details for all entries matching this filter'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (text or json)', 'text'), ]) - ->setDescription('Show a list of twig functions, filters, globals and tests') ->setHelp(<<<'EOF' The %command.name% command outputs a list of twig functions, filters, globals and tests. @@ -99,7 +91,7 @@ protected function configure() ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('name'); @@ -123,6 +115,17 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->getLoaderPaths())); + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['text', 'json']); + } + } + private function displayPathsText(SymfonyStyle $io, string $name) { $file = new \ArrayIterator($this->findTemplateFiles($name)); @@ -306,7 +309,7 @@ private function getLoaderPaths(string $name = null): array return $loaderPaths; } - private function getMetadata(string $type, $entity) + private function getMetadata(string $type, mixed $entity) { if ('globals' === $type) { return $entity; @@ -364,7 +367,7 @@ private function getMetadata(string $type, $entity) return null; } - private function getPrettyMetadata(string $type, $entity, bool $decorated): ?string + private function getPrettyMetadata(string $type, mixed $entity, bool $decorated): ?string { if ('tests' === $type) { return ''; @@ -405,22 +408,6 @@ private function findWrongBundleOverrides(): array $alternatives = []; $bundleNames = []; - if ($this->rootDir && $this->projectDir) { - $folders = glob($this->rootDir.'/Resources/*/views', \GLOB_ONLYDIR); - $relativePath = ltrim(substr($this->rootDir.\DIRECTORY_SEPARATOR.'Resources/', \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); - $bundleNames = array_reduce($folders, function ($carry, $absolutePath) use ($relativePath) { - if (str_starts_with($absolutePath, $this->projectDir)) { - $name = basename(\dirname($absolutePath)); - $path = ltrim($relativePath.$name, \DIRECTORY_SEPARATOR); - $carry[$name] = $path; - - @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $absolutePath, $this->twigDefaultPath.'/bundles/'.$name), \E_USER_DEPRECATED); - } - - return $carry; - }, $bundleNames); - } - if ($this->twigDefaultPath && $this->projectDir) { $folders = glob($this->twigDefaultPath.'/bundles/*', \GLOB_ONLYDIR); $relativePath = ltrim(substr($this->twigDefaultPath.'/bundles/', \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); @@ -586,7 +573,7 @@ private function isAbsolutePath(string $file): bool */ private function getFilesystemLoaders(): array { - if (null !== $this->filesystemLoaders) { + if (isset($this->filesystemLoaders)) { return $this->filesystemLoaders; } $this->filesystemLoaders = []; diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index 9dcbf4964e228..b7295c87840e2 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -11,7 +11,11 @@ namespace Symfony\Bridge\Twig\Command; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; @@ -32,11 +36,11 @@ * @author Marc Weistroff * @author Jérôme Tamarelle */ +#[AsCommand(name: 'lint:twig', description: 'Lint a Twig template and outputs encountered errors')] class LintCommand extends Command { - protected static $defaultName = 'lint:twig'; - private $twig; + private string $format; public function __construct(Environment $twig) { @@ -48,8 +52,7 @@ public function __construct(Environment $twig) protected function configure() { $this - ->setDescription('Lint a template and outputs encountered errors') - ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') ->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors') ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->setHelp(<<<'EOF' @@ -74,24 +77,18 @@ protected function configure() ; } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $filenames = $input->getArgument('filename'); $showDeprecations = $input->getOption('show-deprecations'); + $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); if (['-'] === $filenames) { return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); } if (!$filenames) { - // @deprecated to be removed in 5.0 - if (0 === ftell(\STDIN)) { - @trigger_error('Piping content from STDIN to the "lint:twig" command without passing the dash symbol "-" as argument is deprecated since Symfony 4.4.', \E_USER_DEPRECATED); - - return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); - } - $loader = $this->twig->getLoader(); if ($loader instanceof FilesystemLoader) { $paths = []; @@ -144,7 +141,7 @@ private function getFilesInfo(array $filenames): array return $filesInfo; } - protected function findFiles($filename) + protected function findFiles(string $filename) { if (is_file($filename)) { return [$filename]; @@ -175,26 +172,29 @@ private function validate(string $template, string $file): array private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files) { - switch ($input->getOption('format')) { + switch ($this->format) { case 'txt': return $this->displayTxt($output, $io, $files); case 'json': return $this->displayJson($output, $files); + case 'github': + return $this->displayTxt($output, $io, $files, true); default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); } } - private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo) + private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false) { $errors = 0; + $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($output) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $output->isVerbose()) { $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$errors; - $this->renderException($io, $info['template'], $info['exception'], $info['file']); + $this->renderException($io, $info['template'], $info['exception'], $info['file'], $githubReporter); } } @@ -226,10 +226,14 @@ private function displayJson(OutputInterface $output, array $filesInfo) return min($errors, 1); } - private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null) + private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null, GithubActionReporter $githubReporter = null) { $line = $exception->getTemplateLine(); + if ($githubReporter) { + $githubReporter->error($exception->getRawMessage(), $file, $line <= 0 ? null : $line); + } + if ($file) { $output->text(sprintf(' ERROR in %s (line %s)', $file, $line)); } else { @@ -272,4 +276,11 @@ private function getContext(string $template, int $line, int $context = 3) return $result; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['txt', 'json', 'github']); + } + } } diff --git a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php index fd6bc2d1e2663..ed27f6b89a365 100644 --- a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php +++ b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php @@ -22,17 +22,15 @@ use Twig\Profiler\Profile; /** - * TwigDataCollector. - * * @author Fabien Potencier * - * @final since Symfony 4.4 + * @final */ class TwigDataCollector extends DataCollector implements LateDataCollectorInterface { private $profile; private $twig; - private $computed; + private array $computed; public function __construct(Profile $profile, Environment $twig = null) { @@ -42,10 +40,8 @@ public function __construct(Profile $profile, Environment $twig = null) /** * {@inheritdoc} - * - * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response/*, \Throwable $exception = null*/) + public function collect(Request $request, Response $response, \Throwable $exception = null) { } @@ -55,7 +51,7 @@ public function collect(Request $request, Response $response/*, \Throwable $exce public function reset() { $this->profile->reset(); - $this->computed = null; + unset($this->computed); $this->data = []; } @@ -144,18 +140,12 @@ public function getHtmlCallGraph() public function getProfile() { - if (null === $this->profile) { - $this->profile = unserialize($this->data['profile'], ['allowed_classes' => ['Twig_Profiler_Profile', 'Twig\Profiler\Profile']]); - } - - return $this->profile; + return $this->profile ??= unserialize($this->data['profile'], ['allowed_classes' => ['Twig_Profiler_Profile', 'Twig\Profiler\Profile']]); } private function getComputedData(string $index) { - if (null === $this->computed) { - $this->computed = $this->computeData($this->getProfile()); - } + $this->computed ??= $this->computeData($this->getProfile()); return $this->computed[$index]; } @@ -200,7 +190,7 @@ private function computeData(Profile $profile) /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'twig'; } diff --git a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php index 1f5caee4ee5c7..50cc9cc91d5b3 100644 --- a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php +++ b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php @@ -16,8 +16,6 @@ use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\RequestStack; use Twig\Environment; -use Twig\Error\LoaderError; -use Twig\Loader\ExistsLoaderInterface; /** * Provides the ability to render custom Twig-based HTML error pages @@ -29,20 +27,16 @@ class TwigErrorRenderer implements ErrorRendererInterface { private $twig; private $fallbackErrorRenderer; - private $debug; + private \Closure|bool $debug; /** * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it */ - public function __construct(Environment $twig, HtmlErrorRenderer $fallbackErrorRenderer = null, $debug = false) + public function __construct(Environment $twig, HtmlErrorRenderer $fallbackErrorRenderer = null, bool|callable $debug = false) { - if (!\is_bool($debug) && !\is_callable($debug)) { - throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug))); - } - $this->twig = $twig; $this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer(); - $this->debug = $debug; + $this->debug = !\is_callable($debug) || $debug instanceof \Closure ? $debug : \Closure::fromCallable($debug); } /** @@ -58,7 +52,6 @@ public function render(\Throwable $exception): FlattenException } return $exception->setAsString($this->twig->render($template, [ - 'legacy' => false, // to be removed in 5.0 'exception' => $exception, 'status_code' => $exception->getStatusCode(), 'status_text' => $exception->getStatusText(), @@ -79,39 +72,15 @@ public static function isDebug(RequestStack $requestStack, bool $debug): \Closur private function findTemplate(int $statusCode): ?string { $template = sprintf('@Twig/Exception/error%s.html.twig', $statusCode); - if ($this->templateExists($template)) { + if ($this->twig->getLoader()->exists($template)) { return $template; } $template = '@Twig/Exception/error.html.twig'; - if ($this->templateExists($template)) { + if ($this->twig->getLoader()->exists($template)) { return $template; } return null; } - - /** - * To be removed in 5.0. - * - * Use instead: - * - * $this->twig->getLoader()->exists($template) - */ - private function templateExists(string $template): bool - { - $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { - return $loader->exists($template); - } - - try { - $loader->getSourceContext($template); - - return true; - } catch (LoaderError $e) { - } - - return false; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php index 62e7b91cb2db6..694821f7bf6b8 100644 --- a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -19,10 +19,8 @@ * Twig extension for the Symfony Asset component. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class AssetExtension extends AbstractExtension +final class AssetExtension extends AbstractExtension { private $packages; @@ -33,10 +31,8 @@ public function __construct(Packages $packages) /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('asset', [$this, 'getAssetUrl']), @@ -49,37 +45,17 @@ public function getFunctions() * * If the package used to generate the path is an instance of * UrlPackage, you will always get a URL and not a path. - * - * @param string $path A public path - * @param string $packageName The name of the asset package to use - * - * @return string The public path of the asset */ - public function getAssetUrl($path, $packageName = null) + public function getAssetUrl(string $path, string $packageName = null): string { return $this->packages->getUrl($path, $packageName); } /** * Returns the version of an asset. - * - * @param string $path A public path - * @param string $packageName The name of the asset package to use - * - * @return string The asset version */ - public function getAssetVersion($path, $packageName = null) + public function getAssetVersion(string $path, string $packageName = null): string { return $this->packages->getVersion($path, $packageName); } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() - { - return 'asset'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index 54b43caca2cb3..62573d9f961e6 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -19,21 +19,14 @@ * Twig extension relate to PHP code and used by the profiler and the default exception templates. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class CodeExtension extends AbstractExtension +final class CodeExtension extends AbstractExtension { - private $fileLinkFormat; - private $charset; - private $projectDir; + private string|FileLinkFormatter|array|false $fileLinkFormat; + private string $charset; + private string $projectDir; - /** - * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files - * @param string $projectDir The project directory - * @param string $charset The charset - */ - public function __construct($fileLinkFormat, string $projectDir, string $charset) + public function __construct(string|FileLinkFormatter $fileLinkFormat, string $projectDir, string $charset) { $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->projectDir = str_replace('\\', '/', $projectDir).'/'; @@ -42,10 +35,8 @@ public function __construct($fileLinkFormat, string $projectDir, string $charset /** * {@inheritdoc} - * - * @return TwigFilter[] */ - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html']]), @@ -61,7 +52,7 @@ public function getFilters() ]; } - public function abbrClass($class) + public function abbrClass(string $class): string { $parts = explode('\\', $class); $short = array_pop($parts); @@ -69,7 +60,7 @@ public function abbrClass($class) return sprintf('%s', $class, $short); } - public function abbrMethod($method) + public function abbrMethod(string $method): string { if (str_contains($method, '::')) { [$class, $method] = explode('::', $method, 2); @@ -85,12 +76,8 @@ public function abbrMethod($method) /** * Formats an array as a string. - * - * @param array $args The argument array - * - * @return string */ - public function formatArgs($args) + public function formatArgs(array $args): string { $result = []; foreach ($args as $key => $item) { @@ -118,26 +105,16 @@ public function formatArgs($args) /** * Formats an array as a string. - * - * @param array $args The argument array - * - * @return string */ - public function formatArgsAsText($args) + public function formatArgsAsText(array $args): string { return strip_tags($this->formatArgs($args)); } /** * Returns an excerpt of a code file around the given line number. - * - * @param string $file A file path - * @param int $line The selected line number - * @param int $srcContext The number of displayed lines around or -1 for the whole file - * - * @return string An HTML string */ - public function fileExcerpt($file, $line, $srcContext = 3) + public function fileExcerpt(string $file, int $line, int $srcContext = 3): ?string { if (is_file($file) && is_readable($file)) { // highlight_file could throw warnings @@ -157,7 +134,7 @@ public function fileExcerpt($file, $line, $srcContext = 3) } for ($i = max($line - $srcContext, 1), $max = min($line + $srcContext, \count($content)); $i <= $max; ++$i) { - $lines[] = ''.self::fixCodeMarkup($content[$i - 1]).''; + $lines[] = ''.self::fixCodeMarkup($content[$i - 1]).''; } return '
    '.implode("\n", $lines).'
'; @@ -168,14 +145,8 @@ public function fileExcerpt($file, $line, $srcContext = 3) /** * Formats a file path. - * - * @param string $file An absolute file path - * @param int $line The line number - * @param string $text Use this text for the link rather than the file path - * - * @return string */ - public function formatFile($file, $line, $text = null) + public function formatFile(string $file, int $line, string $text = null): string { $file = trim($file); @@ -198,15 +169,7 @@ public function formatFile($file, $line, $text = null) return $text; } - /** - * Returns the link for a given file/line pair. - * - * @param string $file An absolute file path - * @param int $line The line number - * - * @return string|false A link or false - */ - public function getFileLink($file, $line) + public function getFileLink(string $file, int $line): string|false { if ($fmt = $this->fileLinkFormat) { return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); @@ -226,7 +189,7 @@ public function getFileRelative(string $file): ?string return null; } - public function formatFileFromText($text) + public function formatFileFromText(string $text): string { return preg_replace_callback('/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) { return 'in '.$this->formatFile($match[2], $match[3]); @@ -254,15 +217,7 @@ public function formatLogMessage(string $message, array $context): string return htmlspecialchars($message, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); } - /** - * {@inheritdoc} - */ - public function getName() - { - return 'code'; - } - - protected static function fixCodeMarkup($line) + protected static function fixCodeMarkup(string $line): string { // ending tag from previous line $opening = strpos($line, ' * @author Titouan Galopin - * - * @final since Symfony 4.4 */ -class CsrfExtension extends AbstractExtension +final class CsrfExtension extends AbstractExtension { /** * {@inheritdoc} diff --git a/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php b/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php index ea857c7ed583b..c3d5da6470c25 100644 --- a/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php @@ -16,10 +16,8 @@ /** * @author Christian Flothmann * @author Titouan Galopin - * - * @final since Symfony 4.4 */ -class CsrfRuntime +final class CsrfRuntime { private $csrfTokenManager; diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php index 8937c890e3a99..46ad8eaf679c2 100644 --- a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php @@ -17,17 +17,14 @@ use Twig\Environment; use Twig\Extension\AbstractExtension; use Twig\Template; -use Twig\TokenParser\TokenParserInterface; use Twig\TwigFunction; /** * Provides integration of the dump() function with Twig. * * @author Nicolas Grekas - * - * @final since Symfony 4.4 */ -class DumpExtension extends AbstractExtension +final class DumpExtension extends AbstractExtension { private $cloner; private $dumper; @@ -39,9 +36,9 @@ public function __construct(ClonerInterface $cloner, HtmlDumper $dumper = null) } /** - * @return TwigFunction[] + * {@inheritdoc} */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('dump', [$this, 'dump'], ['is_safe' => ['html'], 'needs_context' => true, 'needs_environment' => true]), @@ -49,19 +46,14 @@ public function getFunctions() } /** - * @return TokenParserInterface[] + * {@inheritdoc} */ - public function getTokenParsers() + public function getTokenParsers(): array { return [new DumpTokenParser()]; } - public function getName() - { - return 'dump'; - } - - public function dump(Environment $env, $context) + public function dump(Environment $env, array $context): ?string { if (!$env->isDebug()) { return null; diff --git a/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php index af7be97c4f9bd..8d2a35c99f682 100644 --- a/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php @@ -19,35 +19,21 @@ * ExpressionExtension gives a way to create Expressions from a template. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class ExpressionExtension extends AbstractExtension +final class ExpressionExtension extends AbstractExtension { /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('expression', [$this, 'createExpression']), ]; } - public function createExpression($expression) + public function createExpression(string $expression): Expression { return new Expression($expression); } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() - { - return 'expression'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 174a5cc3fe4bb..f7d82e042138d 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -12,10 +12,12 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; +use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormView; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; -use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; @@ -25,17 +27,20 @@ * * @author Fabien Potencier * @author Bernhard Schussek - * - * @final since Symfony 4.4 */ -class FormExtension extends AbstractExtension +final class FormExtension extends AbstractExtension { + private $translator; + + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + /** * {@inheritdoc} - * - * @return TokenParserInterface[] */ - public function getTokenParsers() + public function getTokenParsers(): array { return [ // {% form_theme form "SomeBundle::widgets.twig" %} @@ -45,10 +50,8 @@ public function getTokenParsers() /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('form_widget', null, ['node_class' => 'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', 'is_safe' => ['html']]), @@ -62,15 +65,19 @@ public function getFunctions() new TwigFunction('form_end', null, ['node_class' => 'Symfony\Bridge\Twig\Node\RenderBlockNode', 'is_safe' => ['html']]), new TwigFunction('csrf_token', ['Symfony\Component\Form\FormRenderer', 'renderCsrfToken']), new TwigFunction('form_parent', 'Symfony\Bridge\Twig\Extension\twig_get_form_parent'), + new TwigFunction('field_name', [$this, 'getFieldName']), + new TwigFunction('field_value', [$this, 'getFieldValue']), + new TwigFunction('field_label', [$this, 'getFieldLabel']), + new TwigFunction('field_help', [$this, 'getFieldHelp']), + new TwigFunction('field_errors', [$this, 'getFieldErrors']), + new TwigFunction('field_choices', [$this, 'getFieldChoices']), ]; } /** * {@inheritdoc} - * - * @return TwigFilter[] */ - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('humanize', ['Symfony\Component\Form\FormRenderer', 'humanize']), @@ -80,10 +87,8 @@ public function getFilters() /** * {@inheritdoc} - * - * @return TwigTest[] */ - public function getTests() + public function getTests(): array { return [ new TwigTest('selectedchoice', 'Symfony\Bridge\Twig\Extension\twig_is_selected_choice'), @@ -91,12 +96,88 @@ public function getTests() ]; } + public function getFieldName(FormView $view): string + { + $view->setRendered(); + + return $view->vars['full_name']; + } + + public function getFieldValue(FormView $view): string|array + { + return $view->vars['value']; + } + + public function getFieldLabel(FormView $view): ?string + { + if (false === $label = $view->vars['label']) { + return null; + } + + if (!$label && $labelFormat = $view->vars['label_format']) { + $label = str_replace(['%id%', '%name%'], [$view->vars['id'], $view->vars['name']], $labelFormat); + } elseif (!$label) { + $label = ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $view->vars['name'])))); + } + + return $this->createFieldTranslation( + $label, + $view->vars['label_translation_parameters'] ?: [], + $view->vars['translation_domain'] + ); + } + + public function getFieldHelp(FormView $view): ?string + { + return $this->createFieldTranslation( + $view->vars['help'], + $view->vars['help_translation_parameters'] ?: [], + $view->vars['translation_domain'] + ); + } + /** - * {@inheritdoc} + * @return string[] + */ + public function getFieldErrors(FormView $view): iterable + { + /** @var FormError $error */ + foreach ($view->vars['errors'] as $error) { + yield $error->getMessage(); + } + } + + /** + * @return string[]|string[][] */ - public function getName() + public function getFieldChoices(FormView $view): iterable { - return 'form'; + yield from $this->createFieldChoicesList($view->vars['choices'], $view->vars['choice_translation_domain']); + } + + private function createFieldChoicesList(iterable $choices, string|false|null $translationDomain): iterable + { + foreach ($choices as $choice) { + $translatableLabel = $this->createFieldTranslation($choice->label, [], $translationDomain); + + if ($choice instanceof ChoiceGroupView) { + yield $translatableLabel => $this->createFieldChoicesList($choice, $translationDomain); + + continue; + } + + /* @var ChoiceView $choice */ + yield $translatableLabel => $choice->value; + } + } + + private function createFieldTranslation(?string $value, array $parameters, string|false|null $domain): ?string + { + if (!$this->translator || !$value || false === $domain) { + return $value; + } + + return $this->translator->trans($value, $parameters, $domain); } } @@ -105,13 +186,9 @@ public function getName() * * This is a function and not callable due to performance reasons. * - * @param string|array $selectedValue The selected value to compare - * - * @return bool Whether the choice is selected - * * @see ChoiceView::isSelected() */ -function twig_is_selected_choice(ChoiceView $choice, $selectedValue): bool +function twig_is_selected_choice(ChoiceView $choice, string|array $selectedValue): bool { if (\is_array($selectedValue)) { return \in_array($choice->value, $selectedValue, true); diff --git a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php index f3c42482f9d1d..a9ee05c4d0093 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php @@ -12,9 +12,7 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\UrlHelper; -use Symfony\Component\Routing\RequestContext; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -22,47 +20,20 @@ * Twig extension for the Symfony HttpFoundation component. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class HttpFoundationExtension extends AbstractExtension +final class HttpFoundationExtension extends AbstractExtension { private $urlHelper; - /** - * @param UrlHelper $urlHelper - */ - public function __construct($urlHelper) + public function __construct(UrlHelper $urlHelper) { - if ($urlHelper instanceof UrlHelper) { - $this->urlHelper = $urlHelper; - - return; - } - - if (!$urlHelper instanceof RequestStack) { - throw new \TypeError(sprintf('The first argument must be an instance of "%s" or an instance of "%s".', UrlHelper::class, RequestStack::class)); - } - - @trigger_error(sprintf('Passing a "%s" instance as the first argument to the "%s" constructor is deprecated since Symfony 4.3, pass a "%s" instance instead.', RequestStack::class, __CLASS__, UrlHelper::class), \E_USER_DEPRECATED); - - $requestContext = null; - if (2 === \func_num_args()) { - $requestContext = func_get_arg(1); - if (null !== $requestContext && !$requestContext instanceof RequestContext) { - throw new \TypeError(sprintf('The second argument must be an instance of "%s".', RequestContext::class)); - } - } - - $this->urlHelper = new UrlHelper($urlHelper, $requestContext); + $this->urlHelper = $urlHelper; } /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('absolute_url', [$this, 'generateAbsoluteUrl']), @@ -75,13 +46,9 @@ public function getFunctions() * * This method returns the path unchanged if no request is available. * - * @param string $path The path - * - * @return string The absolute URL - * * @see Request::getUriForPath() */ - public function generateAbsoluteUrl($path) + public function generateAbsoluteUrl(string $path): string { return $this->urlHelper->getAbsoluteUrl($path); } @@ -91,24 +58,10 @@ public function generateAbsoluteUrl($path) * * This method returns the path unchanged if no request is available. * - * @param string $path The path - * - * @return string The relative path - * * @see Request::getRelativeUriForPath() */ - public function generateRelativePath($path) + public function generateRelativePath(string $path): string { return $this->urlHelper->getRelativePath($path); } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() - { - return 'request'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php index 286bc420c66c5..1af9ddb23cf51 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php @@ -19,33 +19,24 @@ * Provides integration with the HttpKernel component. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class HttpKernelExtension extends AbstractExtension +final class HttpKernelExtension extends AbstractExtension { /** - * @return TwigFunction[] + * {@inheritdoc} */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('render', [HttpKernelRuntime::class, 'renderFragment'], ['is_safe' => ['html']]), new TwigFunction('render_*', [HttpKernelRuntime::class, 'renderFragmentStrategy'], ['is_safe' => ['html']]), + new TwigFunction('fragment_uri', [HttpKernelRuntime::class, 'generateFragmentUri']), new TwigFunction('controller', static::class.'::controller'), ]; } - public static function controller($controller, $attributes = [], $query = []) + public static function controller(string $controller, array $attributes = [], array $query = []): ControllerReference { return new ControllerReference($controller, $attributes, $query); } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'http_kernel'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php index edcf8a4dc0e6f..7c86d7dd40add 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php @@ -13,34 +13,30 @@ use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; +use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; /** * Provides integration with the HttpKernel component. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class HttpKernelRuntime +final class HttpKernelRuntime { private $handler; + private $fragmentUriGenerator; - public function __construct(FragmentHandler $handler) + public function __construct(FragmentHandler $handler, FragmentUriGeneratorInterface $fragmentUriGenerator = null) { $this->handler = $handler; + $this->fragmentUriGenerator = $fragmentUriGenerator; } /** * Renders a fragment. * - * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance - * @param array $options An array of options - * - * @return string The fragment content - * * @see FragmentHandler::render() */ - public function renderFragment($uri, $options = []) + public function renderFragment(string|ControllerReference $uri, array $options = []): string { $strategy = $options['strategy'] ?? 'inline'; unset($options['strategy']); @@ -51,16 +47,19 @@ public function renderFragment($uri, $options = []) /** * Renders a fragment. * - * @param string $strategy A strategy name - * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance - * @param array $options An array of options - * - * @return string The fragment content - * * @see FragmentHandler::render() */ - public function renderFragmentStrategy($strategy, $uri, $options = []) + public function renderFragmentStrategy(string $strategy, string|ControllerReference $uri, array $options = []): string { return $this->handler->render($uri, $strategy, $options); } + + public function generateFragmentUri(ControllerReference $controller, bool $absolute = false, bool $strict = true, bool $sign = true): string + { + if (null === $this->fragmentUriGenerator) { + throw new \LogicException(sprintf('An instance of "%s" must be provided to use "%s()".', FragmentUriGeneratorInterface::class, __METHOD__)); + } + + return $this->fragmentUriGenerator->generate($controller, null, $absolute, $strict, $sign); + } } diff --git a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php index a6648dc072db1..071b9ff247f1d 100644 --- a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php @@ -19,10 +19,8 @@ * LogoutUrlHelper provides generator functions for the logout URL to Twig. * * @author Jeremy Mikola - * - * @final since Symfony 4.4 */ -class LogoutUrlExtension extends AbstractExtension +final class LogoutUrlExtension extends AbstractExtension { private $generator; @@ -33,10 +31,8 @@ public function __construct(LogoutUrlGenerator $generator) /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('logout_url', [$this, 'getLogoutUrl']), @@ -48,10 +44,8 @@ public function getFunctions() * Generates the relative logout URL for the firewall. * * @param string|null $key The firewall key or null to use the current firewall key - * - * @return string The relative logout URL */ - public function getLogoutPath($key = null) + public function getLogoutPath(string $key = null): string { return $this->generator->getLogoutPath($key); } @@ -60,19 +54,9 @@ public function getLogoutPath($key = null) * Generates the absolute logout URL for the firewall. * * @param string|null $key The firewall key or null to use the current firewall key - * - * @return string The absolute logout URL */ - public function getLogoutUrl($key = null) + public function getLogoutUrl(string $key = null): string { return $this->generator->getLogoutUrl($key); } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'logout_url'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php index a46f2cdbb8936..cba3ab8d46329 100644 --- a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php @@ -12,18 +12,21 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Stopwatch\StopwatchEvent; use Twig\Extension\ProfilerExtension as BaseProfilerExtension; use Twig\Profiler\Profile; /** * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class ProfilerExtension extends BaseProfilerExtension +final class ProfilerExtension extends BaseProfilerExtension { private $stopwatch; - private $events; + + /** + * @var \SplObjectStorage + */ + private \SplObjectStorage $events; public function __construct(Profile $profile, Stopwatch $stopwatch = null) { @@ -33,10 +36,7 @@ public function __construct(Profile $profile, Stopwatch $stopwatch = null) $this->events = new \SplObjectStorage(); } - /** - * @return void - */ - public function enter(Profile $profile) + public function enter(Profile $profile): void { if ($this->stopwatch && $profile->isTemplate()) { $this->events[$profile] = $this->stopwatch->start($profile->getName(), 'template'); @@ -45,10 +45,7 @@ public function enter(Profile $profile) parent::enter($profile); } - /** - * @return void - */ - public function leave(Profile $profile) + public function leave(Profile $profile): void { parent::leave($profile); @@ -57,12 +54,4 @@ public function leave(Profile $profile) unset($this->events[$profile]); } } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'native_profiler'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php index 1ba528546d6d2..800c22f6d4c2c 100644 --- a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php @@ -22,10 +22,8 @@ * Provides integration of the Routing component with Twig. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class RoutingExtension extends AbstractExtension +final class RoutingExtension extends AbstractExtension { private $generator; @@ -36,10 +34,8 @@ public function __construct(UrlGeneratorInterface $generator) /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('url', [$this, 'getUrl'], ['is_safe_callback' => [$this, 'isUrlGenerationSafe']]), @@ -47,26 +43,12 @@ public function getFunctions() ]; } - /** - * @param string $name - * @param array $parameters - * @param bool $relative - * - * @return string - */ - public function getPath($name, $parameters = [], $relative = false) + public function getPath(string $name, array $parameters = [], bool $relative = false): string { return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH); } - /** - * @param string $name - * @param array $parameters - * @param bool $schemeRelative - * - * @return string - */ - public function getUrl($name, $parameters = [], $schemeRelative = false) + public function getUrl(string $name, array $parameters = [], bool $schemeRelative = false): string { return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL); } @@ -92,8 +74,6 @@ public function getUrl($name, $parameters = [], $schemeRelative = false) * @param Node $argsNode The arguments of the path/url function * * @return array An array with the contexts the URL is safe - * - * @final */ public function isUrlGenerationSafe(Node $argsNode): array { @@ -110,12 +90,4 @@ public function isUrlGenerationSafe(Node $argsNode): array return []; } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'routing'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index 4acd7bbf9cc72..aedeefdca9d52 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -14,6 +14,7 @@ use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; +use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -21,19 +22,19 @@ * SecurityExtension exposes security context features. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class SecurityExtension extends AbstractExtension +final class SecurityExtension extends AbstractExtension { private $securityChecker; + private $impersonateUrlGenerator; - public function __construct(AuthorizationCheckerInterface $securityChecker = null) + public function __construct(AuthorizationCheckerInterface $securityChecker = null, ImpersonateUrlGenerator $impersonateUrlGenerator = null) { $this->securityChecker = $securityChecker; + $this->impersonateUrlGenerator = $impersonateUrlGenerator; } - public function isGranted($role, $object = null, $field = null) + public function isGranted(mixed $role, mixed $object = null, string $field = null): bool { if (null === $this->securityChecker) { return false; @@ -50,23 +51,33 @@ public function isGranted($role, $object = null, $field = null) } } - /** - * {@inheritdoc} - * - * @return TwigFunction[] - */ - public function getFunctions() + public function getImpersonateExitUrl(string $exitTo = null): string { - return [ - new TwigFunction('is_granted', [$this, 'isGranted']), - ]; + if (null === $this->impersonateUrlGenerator) { + return ''; + } + + return $this->impersonateUrlGenerator->generateExitUrl($exitTo); + } + + public function getImpersonateExitPath(string $exitTo = null): string + { + if (null === $this->impersonateUrlGenerator) { + return ''; + } + + return $this->impersonateUrlGenerator->generateExitPath($exitTo); } /** * {@inheritdoc} */ - public function getName() + public function getFunctions(): array { - return 'security'; + return [ + new TwigFunction('is_granted', [$this, 'isGranted']), + new TwigFunction('impersonation_exit_url', [$this, 'getImpersonateExitUrl']), + new TwigFunction('impersonation_exit_path', [$this, 'getImpersonateExitPath']), + ]; } } diff --git a/src/Symfony/Bridge/Twig/Extension/SerializerExtension.php b/src/Symfony/Bridge/Twig/Extension/SerializerExtension.php new file mode 100644 index 0000000000000..f38571efaaac8 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/SerializerExtension.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; + +/** + * @author Jesse Rushlow + */ +final class SerializerExtension extends AbstractExtension +{ + public function getFilters(): array + { + return [ + new TwigFilter('serialize', [SerializerRuntime::class, 'serialize']), + ]; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php b/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php new file mode 100644 index 0000000000000..dbffa31c2741b --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/SerializerRuntime.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Extension; + +use Symfony\Component\Serializer\SerializerInterface; +use Twig\Extension\RuntimeExtensionInterface; + +/** + * @author Jesse Rushlow + */ +final class SerializerRuntime implements RuntimeExtensionInterface +{ + private $serializer; + + public function __construct(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } + + public function serialize(mixed $data, string $format = 'json', array $context = []): string + { + return $this->serializer->serialize($data, $format, $context); + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php index f4b9a24ced5cd..635216f23203b 100644 --- a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php @@ -20,13 +20,11 @@ * Twig extension for the stopwatch helper. * * @author Wouter J - * - * @final since Symfony 4.4 */ -class StopwatchExtension extends AbstractExtension +final class StopwatchExtension extends AbstractExtension { private $stopwatch; - private $enabled; + private bool $enabled; public function __construct(Stopwatch $stopwatch = null, bool $enabled = true) { @@ -34,7 +32,7 @@ public function __construct(Stopwatch $stopwatch = null, bool $enabled = true) $this->enabled = $enabled; } - public function getStopwatch() + public function getStopwatch(): Stopwatch { return $this->stopwatch; } @@ -42,7 +40,7 @@ public function getStopwatch() /** * @return TokenParserInterface[] */ - public function getTokenParsers() + public function getTokenParsers(): array { return [ /* @@ -53,9 +51,4 @@ public function getTokenParsers() new StopwatchTokenParser(null !== $this->stopwatch && $this->enabled), ]; } - - public function getName() - { - return 'stopwatch'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index 96df106307da7..fa5c66fd43c63 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -13,16 +13,15 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; -use Symfony\Bridge\Twig\TokenParser\TransChoiceTokenParser; use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser; use Symfony\Bridge\Twig\TokenParser\TransTokenParser; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; use Twig\Extension\AbstractExtension; -use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\TokenParser\AbstractTokenParser; use Twig\TwigFilter; +use Twig\TwigFunction; // Help opcache.preload discover always-needed symbols class_exists(TranslatorInterface::class); @@ -32,30 +31,19 @@ class_exists(TranslatorTrait::class); * Provides integration of the Translation component with Twig. * * @author Fabien Potencier - * - * @final since Symfony 4.2 */ -class TranslationExtension extends AbstractExtension +final class TranslationExtension extends AbstractExtension { private $translator; private $translationNodeVisitor; - /** - * @param TranslatorInterface|null $translator - */ - public function __construct($translator = null, NodeVisitorInterface $translationNodeVisitor = null) + public function __construct(TranslatorInterface $translator = null, TranslationNodeVisitor $translationNodeVisitor = null) { - if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } $this->translator = $translator; $this->translationNodeVisitor = $translationNodeVisitor; } - /** - * @return TranslatorInterface|null - */ - public function getTranslator() + public function getTranslator(): TranslatorInterface { if (null === $this->translator) { if (!interface_exists(TranslatorInterface::class)) { @@ -72,33 +60,33 @@ public function getTranslator() /** * {@inheritdoc} - * - * @return TwigFilter[] */ - public function getFilters() + public function getFunctions(): array + { + return [ + new TwigFunction('t', [$this, 'createTranslatable']), + ]; + } + + /** + * {@inheritdoc} + */ + public function getFilters(): array { return [ new TwigFilter('trans', [$this, 'trans']), - new TwigFilter('transchoice', [$this, 'transchoice'], ['deprecated' => '4.2', 'alternative' => 'trans" with parameter "%count%']), ]; } /** - * Returns the token parser instance to add to the existing list. - * - * @return AbstractTokenParser[] + * {@inheritdoc} */ - public function getTokenParsers() + public function getTokenParsers(): array { return [ // {% trans %}Symfony is great!{% endtrans %} new TransTokenParser(), - // {% transchoice count %} - // {0} There is no apples|{1} There is one apple|]1,Inf] There is {{ count }} apples - // {% endtranschoice %} - new TransChoiceTokenParser(), - // {% trans_default_domain "foobar" %} new TransDefaultDomainTokenParser(), ]; @@ -106,21 +94,38 @@ public function getTokenParsers() /** * {@inheritdoc} - * - * @return NodeVisitorInterface[] */ - public function getNodeVisitors() + public function getNodeVisitors(): array { return [$this->getTranslationNodeVisitor(), new TranslationDefaultDomainNodeVisitor()]; } - public function getTranslationNodeVisitor() + public function getTranslationNodeVisitor(): TranslationNodeVisitor { return $this->translationNodeVisitor ?: $this->translationNodeVisitor = new TranslationNodeVisitor(); } - public function trans($message, array $arguments = [], $domain = null, $locale = null, $count = null) + /** + * @param array|string $arguments Can be the locale as a string when $message is a TranslatableInterface + */ + public function trans(string|\Stringable|TranslatableInterface|null $message, array|string $arguments = [], string $domain = null, string $locale = null, int $count = null): string { + if ($message instanceof TranslatableInterface) { + if ([] !== $arguments && !\is_string($arguments)) { + throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be a locale passed as a string when the message is a "%s", "%s" given.', __METHOD__, TranslatableInterface::class, get_debug_type($arguments))); + } + + return $message->trans($this->getTranslator(), $locale ?? (\is_string($arguments) ? $arguments : null)); + } + + if (!\is_array($arguments)) { + throw new \TypeError(sprintf('Unless the message is a "%s", argument 2 passed to "%s()" must be an array of parameters, "%s" given.', TranslatableInterface::class, __METHOD__, get_debug_type($arguments))); + } + + if ('' === $message = (string) $message) { + return ''; + } + if (null !== $count) { $arguments['%count%'] = $count; } @@ -128,25 +133,12 @@ public function trans($message, array $arguments = [], $domain = null, $locale = return $this->getTranslator()->trans($message, $arguments, $domain, $locale); } - /** - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter - */ - public function transchoice($message, $count, array $arguments = [], $domain = null, $locale = null) + public function createTranslatable(string $message, array $parameters = [], string $domain = null): TranslatableMessage { - $translator = $this->getTranslator(); - - if ($translator instanceof TranslatorInterface) { - return $translator->trans($message, array_merge(['%count%' => $count], $arguments), $domain, $locale); + if (!class_exists(TranslatableMessage::class)) { + throw new \LogicException(sprintf('You cannot use the "%s" as the Translation Component is not installed. Try running "composer require symfony/translation".', __CLASS__)); } - return $translator->transChoice($message, $count, array_merge(['%count%' => $count], $arguments), $domain, $locale); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'translator'; + return new TranslatableMessage($message, $parameters, $domain); } } diff --git a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php index c2c6f8ba8fcf6..652a75762c63b 100644 --- a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php @@ -21,10 +21,8 @@ * Twig extension for the Symfony WebLink component. * * @author Kévin Dunglas - * - * @final since Symfony 4.4 */ -class WebLinkExtension extends AbstractExtension +final class WebLinkExtension extends AbstractExtension { private $requestStack; @@ -35,10 +33,8 @@ public function __construct(RequestStack $requestStack) /** * {@inheritdoc} - * - * @return TwigFunction[] */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('link', [$this, 'link']), @@ -53,15 +49,14 @@ public function getFunctions() /** * Adds a "Link" HTTP header. * - * @param string $uri The relation URI * @param string $rel The relation type (e.g. "preload", "prefetch", "prerender" or "dns-prefetch") * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * * @return string The relation URI */ - public function link($uri, $rel, array $attributes = []) + public function link(string $uri, string $rel, array $attributes = []): string { - if (!$request = $this->requestStack->getMasterRequest()) { + if (!$request = $this->requestStack->getMainRequest()) { return $uri; } @@ -79,12 +74,11 @@ public function link($uri, $rel, array $attributes = []) /** * Preloads a resource. * - * @param string $uri A public path - * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['crossorigin' => 'use-credentials']") + * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['crossorigin' => 'use-credentials']") * * @return string The path of the asset */ - public function preload($uri, array $attributes = []) + public function preload(string $uri, array $attributes = []): string { return $this->link($uri, 'preload', $attributes); } @@ -92,12 +86,11 @@ public function preload($uri, array $attributes = []) /** * Resolves a resource origin as early as possible. * - * @param string $uri A public path - * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") + * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * * @return string The path of the asset */ - public function dnsPrefetch($uri, array $attributes = []) + public function dnsPrefetch(string $uri, array $attributes = []): string { return $this->link($uri, 'dns-prefetch', $attributes); } @@ -105,12 +98,11 @@ public function dnsPrefetch($uri, array $attributes = []) /** * Initiates a early connection to a resource (DNS resolution, TCP handshake, TLS negotiation). * - * @param string $uri A public path - * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") + * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * * @return string The path of the asset */ - public function preconnect($uri, array $attributes = []) + public function preconnect(string $uri, array $attributes = []): string { return $this->link($uri, 'preconnect', $attributes); } @@ -118,12 +110,11 @@ public function preconnect($uri, array $attributes = []) /** * Indicates to the client that it should prefetch this resource. * - * @param string $uri A public path - * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") + * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * * @return string The path of the asset */ - public function prefetch($uri, array $attributes = []) + public function prefetch(string $uri, array $attributes = []): string { return $this->link($uri, 'prefetch', $attributes); } @@ -131,12 +122,11 @@ public function prefetch($uri, array $attributes = []) /** * Indicates to the client that it should prerender this resource . * - * @param string $uri A public path - * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") + * @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]") * * @return string The path of the asset */ - public function prerender($uri, array $attributes = []) + public function prerender(string $uri, array $attributes = []): string { return $this->link($uri, 'prerender', $attributes); } diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php index b5f3badea88fd..3ee1ac3d6b6b2 100644 --- a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -21,10 +21,9 @@ * WorkflowExtension. * * @author Grégoire Pineau - * - * @final since Symfony 4.4 + * @author Carlos Pereira De Amorim */ -class WorkflowExtension extends AbstractExtension +final class WorkflowExtension extends AbstractExtension { private $workflowRegistry; @@ -34,13 +33,14 @@ public function __construct(Registry $workflowRegistry) } /** - * @return TwigFunction[] + * {@inheritdoc} */ - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('workflow_can', [$this, 'canTransition']), new TwigFunction('workflow_transitions', [$this, 'getEnabledTransitions']), + new TwigFunction('workflow_transition', [$this, 'getEnabledTransition']), new TwigFunction('workflow_has_marked_place', [$this, 'hasMarkedPlace']), new TwigFunction('workflow_marked_places', [$this, 'getMarkedPlaces']), new TwigFunction('workflow_metadata', [$this, 'getMetadata']), @@ -50,14 +50,8 @@ public function getFunctions() /** * Returns true if the transition is enabled. - * - * @param object $subject A subject - * @param string $transitionName A transition - * @param string $name A workflow name - * - * @return bool true if the transition is enabled */ - public function canTransition($subject, $transitionName, $name = null) + public function canTransition(object $subject, string $transitionName, string $name = null): bool { return $this->workflowRegistry->get($subject, $name)->can($subject, $transitionName); } @@ -65,26 +59,22 @@ public function canTransition($subject, $transitionName, $name = null) /** * Returns all enabled transitions. * - * @param object $subject A subject - * @param string $name A workflow name - * - * @return Transition[] All enabled transitions + * @return Transition[] */ - public function getEnabledTransitions($subject, $name = null) + public function getEnabledTransitions(object $subject, string $name = null): array { return $this->workflowRegistry->get($subject, $name)->getEnabledTransitions($subject); } + public function getEnabledTransition(object $subject, string $transition, string $name = null): ?Transition + { + return $this->workflowRegistry->get($subject, $name)->getEnabledTransition($subject, $transition); + } + /** * Returns true if the place is marked. - * - * @param object $subject A subject - * @param string $placeName A place name - * @param string $name A workflow name - * - * @return bool true if the transition is enabled */ - public function hasMarkedPlace($subject, $placeName, $name = null) + public function hasMarkedPlace(object $subject, string $placeName, string $name = null): bool { return $this->workflowRegistry->get($subject, $name)->getMarking($subject)->has($placeName); } @@ -92,13 +82,9 @@ public function hasMarkedPlace($subject, $placeName, $name = null) /** * Returns marked places. * - * @param object $subject A subject - * @param bool $placesNameOnly If true, returns only places name. If false returns the raw representation - * @param string $name A workflow name - * * @return string[]|int[] */ - public function getMarkedPlaces($subject, $placesNameOnly = true, $name = null) + public function getMarkedPlaces(object $subject, bool $placesNameOnly = true, string $name = null): array { $places = $this->workflowRegistry->get($subject, $name)->getMarking($subject)->getPlaces(); @@ -112,12 +98,11 @@ public function getMarkedPlaces($subject, $placesNameOnly = true, $name = null) /** * Returns the metadata for a specific subject. * - * @param object $subject A subject * @param string|Transition|null $metadataSubject Use null to get workflow metadata * Use a string (the place name) to get place metadata * Use a Transition instance to get transition metadata */ - public function getMetadata($subject, string $key, $metadataSubject = null, string $name = null) + public function getMetadata(object $subject, string $key, string|Transition $metadataSubject = null, string $name = null) { return $this ->workflowRegistry @@ -127,15 +112,10 @@ public function getMetadata($subject, string $key, $metadataSubject = null, stri ; } - public function buildTransitionBlockerList($subject, string $transitionName, string $name = null): TransitionBlockerList + public function buildTransitionBlockerList(object $subject, string $transitionName, string $name = null): TransitionBlockerList { $workflow = $this->workflowRegistry->get($subject, $name); return $workflow->buildTransitionBlockerList($subject, $transitionName); } - - public function getName() - { - return 'workflow'; - } } diff --git a/src/Symfony/Bridge/Twig/Extension/YamlExtension.php b/src/Symfony/Bridge/Twig/Extension/YamlExtension.php index 02d19d8ae3d3b..919834e24a5d6 100644 --- a/src/Symfony/Bridge/Twig/Extension/YamlExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/YamlExtension.php @@ -12,7 +12,6 @@ namespace Symfony\Bridge\Twig\Extension; use Symfony\Component\Yaml\Dumper as YamlDumper; -use Symfony\Component\Yaml\Yaml; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; @@ -20,17 +19,13 @@ * Provides integration of the Yaml component with Twig. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class YamlExtension extends AbstractExtension +final class YamlExtension extends AbstractExtension { /** * {@inheritdoc} - * - * @return TwigFilter[] */ - public function getFilters() + public function getFilters(): array { return [ new TwigFilter('yaml_encode', [$this, 'encode']), @@ -38,7 +33,7 @@ public function getFilters() ]; } - public function encode($input, $inline = 0, $dumpObjects = 0) + public function encode(mixed $input, int $inline = 0, int $dumpObjects = 0): string { static $dumper; @@ -53,7 +48,7 @@ public function encode($input, $inline = 0, $dumpObjects = 0) return $dumper->dump($input, $inline, 0, false, $dumpObjects); } - public function dump($value, $inline = 0, $dumpObjects = false) + public function dump(mixed $value, int $inline = 0, int $dumpObjects = 0): string { if (\is_resource($value)) { return '%Resource%'; @@ -65,12 +60,4 @@ public function dump($value, $inline = 0, $dumpObjects = false) return $this->encode($value, $inline, $dumpObjects); } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'yaml'; - } } diff --git a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php index 1e97ce3371d1d..693ee81239924 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php @@ -21,14 +21,7 @@ */ class TwigRendererEngine extends AbstractRendererEngine { - /** - * @var Environment - */ private $environment; - - /** - * @var Template - */ private $template; public function __construct(array $defaultThemes, Environment $environment) @@ -40,7 +33,7 @@ public function __construct(array $defaultThemes, Environment $environment) /** * {@inheritdoc} */ - public function renderBlock(FormView $view, $resource, $blockName, array $variables = []) + public function renderBlock(FormView $view, mixed $resource, string $blockName, array $variables = []): string { $cacheKey = $view->vars[self::CACHE_KEY_VAR]; @@ -69,14 +62,8 @@ public function renderBlock(FormView $view, $resource, $blockName, array $variab * case that the function "block()" is used in the Twig template. * * @see getResourceForBlock() - * - * @param string $cacheKey The cache key of the form view - * @param FormView $view The form view for finding the applying themes - * @param string $blockName The name of the block to load - * - * @return bool True if the resource could be loaded, false otherwise */ - protected function loadResourceForBlockName($cacheKey, FormView $view, $blockName) + protected function loadResourceForBlockName(string $cacheKey, FormView $view, string $blockName): bool { // The caller guarantees that $this->resources[$cacheKey][$block] is // not set, but it doesn't have to check whether $this->resources[$cacheKey] @@ -143,27 +130,23 @@ protected function loadResourceForBlockName($cacheKey, FormView $view, $blockNam /** * Loads the resources for all blocks in a theme. * - * @param string $cacheKey The cache key for storing the resource - * @param mixed $theme The theme to load the block from. This parameter - * is passed by reference, because it might be necessary - * to initialize the theme first. Any changes made to - * this variable will be kept and be available upon - * further calls to this method using the same theme. + * @param mixed $theme The theme to load the block from. This parameter + * is passed by reference, because it might be necessary + * to initialize the theme first. Any changes made to + * this variable will be kept and be available upon + * further calls to this method using the same theme. */ - protected function loadResourcesFromTheme($cacheKey, &$theme) + protected function loadResourcesFromTheme(string $cacheKey, mixed &$theme) { if (!$theme instanceof Template) { - /* @var Template $theme */ $theme = $this->environment->load($theme)->unwrap(); } - if (null === $this->template) { - // Store the first Template instance that we find so that - // we can call displayBlock() later on. It doesn't matter *which* - // template we use for that, since we pass the used blocks manually - // anyway. - $this->template = $theme; - } + // Store the first Template instance that we find so that + // we can call displayBlock() later on. It doesn't matter *which* + // template we use for that, since we pass the used blocks manually + // anyway. + $this->template ??= $theme; // Use a separate variable for the inheritance traversal, because // theme is a reference and we don't want to change it. diff --git a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php index 166b3c195ff17..e50efa06f6efe 100644 --- a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php +++ b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php @@ -23,7 +23,7 @@ final class BodyRenderer implements BodyRendererInterface { private $twig; - private $context; + private array $context; private $converter; public function __construct(Environment $twig, array $context = []) @@ -55,7 +55,7 @@ public function render(Message $message): void } if (isset($messageContext['email'])) { - throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', \get_class($message))); + throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', get_debug_type($message))); } $vars = array_merge($this->context, $messageContext, [ @@ -96,7 +96,7 @@ private function getFingerPrint(TemplatedEmail $message): string private function convertHtmlToText(string $html): string { - if (null !== $this->converter) { + if (isset($this->converter)) { return $this->converter->convert($html); } diff --git a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php index 1a58aa5e5e5bc..8e71df1b0e206 100644 --- a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php @@ -28,8 +28,8 @@ class NotificationEmail extends TemplatedEmail public const IMPORTANCE_MEDIUM = 'medium'; public const IMPORTANCE_LOW = 'low'; - private $theme = 'default'; - private $context = [ + private string $theme = 'default'; + private array $context = [ 'importance' => self::IMPORTANCE_LOW, 'content' => '', 'exception' => false, @@ -37,6 +37,7 @@ class NotificationEmail extends TemplatedEmail 'action_url' => null, 'markdown' => false, 'raw' => false, + 'footer_text' => 'Notification e-mail sent by Symfony', ]; public function __construct(Headers $headers = null, AbstractPart $body = null) @@ -57,10 +58,32 @@ public function __construct(Headers $headers = null, AbstractPart $body = null) parent::__construct($headers, $body); } + /** + * Creates a NotificationEmail instance that is appropriate to send to normal (non-admin) users. + */ + public static function asPublicEmail(Headers $headers = null, AbstractPart $body = null): self + { + $email = new static($headers, $body); + $email->markAsPublic(); + + return $email; + } + /** * @return $this */ - public function markdown(string $content) + public function markAsPublic(): static + { + $this->context['importance'] = null; + $this->context['footer_text'] = null; + + return $this; + } + + /** + * @return $this + */ + public function markdown(string $content): static { if (!class_exists(MarkdownExtension::class)) { throw new \LogicException(sprintf('You cannot use "%s" if the Markdown Twig extension is not available; try running "composer require twig/markdown-extra".', __METHOD__)); @@ -74,7 +97,7 @@ public function markdown(string $content) /** * @return $this */ - public function content(string $content, bool $raw = false) + public function content(string $content, bool $raw = false): static { $this->context['content'] = $content; $this->context['raw'] = $raw; @@ -85,7 +108,7 @@ public function content(string $content, bool $raw = false) /** * @return $this */ - public function action(string $text, string $url) + public function action(string $text, string $url): static { $this->context['action_text'] = $text; $this->context['action_url'] = $url; @@ -96,7 +119,7 @@ public function action(string $text, string $url) /** * @return $this */ - public function importance(string $importance) + public function importance(string $importance): static { $this->context['importance'] = $importance; @@ -104,16 +127,10 @@ public function importance(string $importance) } /** - * @param \Throwable|FlattenException $exception - * * @return $this */ - public function exception($exception) + public function exception(\Throwable|FlattenException $exception): static { - if (!$exception instanceof \Throwable && !$exception instanceof FlattenException) { - throw new \LogicException(sprintf('"%s" accepts "%s" or "%s" instances.', __METHOD__, \Throwable::class, FlattenException::class)); - } - $exceptionAsString = $this->getExceptionAsString($exception); $this->context['exception'] = true; @@ -130,7 +147,7 @@ public function exception($exception) /** * @return $this */ - public function theme(string $theme) + public function theme(string $theme): static { $this->theme = $theme; @@ -166,7 +183,9 @@ public function getPreparedHeaders(): Headers $importance = $this->context['importance'] ?? self::IMPORTANCE_LOW; $this->priority($this->determinePriority($importance)); - $headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject())); + if ($this->context['importance']) { + $headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject())); + } return $headers; } @@ -186,7 +205,7 @@ private function determinePriority(string $importance): int } } - private function getExceptionAsString($exception): string + private function getExceptionAsString(\Throwable|FlattenException $exception): string { if (class_exists(FlattenException::class)) { $exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception); diff --git a/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php b/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php index 6dd9202de8fc7..083b007726310 100644 --- a/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php @@ -18,14 +18,14 @@ */ class TemplatedEmail extends Email { - private $htmlTemplate; - private $textTemplate; - private $context = []; + private ?string $htmlTemplate = null; + private ?string $textTemplate = null; + private array $context = []; /** * @return $this */ - public function textTemplate(?string $template) + public function textTemplate(?string $template): static { $this->textTemplate = $template; @@ -35,7 +35,7 @@ public function textTemplate(?string $template) /** * @return $this */ - public function htmlTemplate(?string $template) + public function htmlTemplate(?string $template): static { $this->htmlTemplate = $template; @@ -55,7 +55,7 @@ public function getHtmlTemplate(): ?string /** * @return $this */ - public function context(array $context) + public function context(array $context): static { $this->context = $context; diff --git a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php index f1726914b490b..b567667883ef3 100644 --- a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php @@ -60,7 +60,7 @@ public function attach(string $file, string $name = null, string $contentType = /** * @return $this */ - public function setSubject(string $subject): self + public function setSubject(string $subject): static { $this->message->subject($subject); @@ -75,7 +75,7 @@ public function getSubject(): ?string /** * @return $this */ - public function setReturnPath(string $address): self + public function setReturnPath(string $address): static { $this->message->returnPath($address); @@ -90,7 +90,7 @@ public function getReturnPath(): string /** * @return $this */ - public function addFrom(string $address, string $name = ''): self + public function addFrom(string $address, string $name = ''): static { $this->message->addFrom(new Address($address, $name)); @@ -108,7 +108,7 @@ public function getFrom(): array /** * @return $this */ - public function addReplyTo(string $address): self + public function addReplyTo(string $address): static { $this->message->addReplyTo($address); @@ -126,7 +126,7 @@ public function getReplyTo(): array /** * @return $this */ - public function addTo(string $address, string $name = ''): self + public function addTo(string $address, string $name = ''): static { $this->message->addTo(new Address($address, $name)); @@ -144,7 +144,7 @@ public function getTo(): array /** * @return $this */ - public function addCc(string $address, string $name = ''): self + public function addCc(string $address, string $name = ''): static { $this->message->addCc(new Address($address, $name)); @@ -162,7 +162,7 @@ public function getCc(): array /** * @return $this */ - public function addBcc(string $address, string $name = ''): self + public function addBcc(string $address, string $name = ''): static { $this->message->addBcc(new Address($address, $name)); @@ -180,7 +180,7 @@ public function getBcc(): array /** * @return $this */ - public function setPriority(int $priority): self + public function setPriority(int $priority): static { $this->message->priority($priority); diff --git a/src/Symfony/Bridge/Twig/Node/DumpNode.php b/src/Symfony/Bridge/Twig/Node/DumpNode.php index d82d9ade1feaf..8ce2bd8c4fa51 100644 --- a/src/Symfony/Bridge/Twig/Node/DumpNode.php +++ b/src/Symfony/Bridge/Twig/Node/DumpNode.php @@ -16,14 +16,12 @@ /** * @author Julien Galenski - * - * @final since Symfony 4.4 */ -class DumpNode extends Node +final class DumpNode extends Node { - private $varPrefix; + private string $varPrefix; - public function __construct($varPrefix, Node $values = null, int $lineno, string $tag = null) + public function __construct(string $varPrefix, ?Node $values, int $lineno, string $tag = null) { $nodes = []; if (null !== $values) { @@ -34,10 +32,7 @@ public function __construct($varPrefix, Node $values = null, int $lineno, string $this->varPrefix = $varPrefix; } - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { $compiler ->write("if (\$this->env->isDebug()) {\n") diff --git a/src/Symfony/Bridge/Twig/Node/FormThemeNode.php b/src/Symfony/Bridge/Twig/Node/FormThemeNode.php index b17243060f302..e37311267bb17 100644 --- a/src/Symfony/Bridge/Twig/Node/FormThemeNode.php +++ b/src/Symfony/Bridge/Twig/Node/FormThemeNode.php @@ -17,20 +17,15 @@ /** * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class FormThemeNode extends Node +final class FormThemeNode extends Node { public function __construct(Node $form, Node $resources, int $lineno, string $tag = null, bool $only = false) { parent::__construct(['form' => $form, 'resources' => $resources], ['only' => $only], $lineno, $tag); } - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) diff --git a/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php b/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php index 29402a8024fae..4d4cf61365772 100644 --- a/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php +++ b/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php @@ -21,15 +21,10 @@ * is "foo", the block "foo" will be rendered. * * @author Bernhard Schussek - * - * @final since Symfony 4.4 */ -class RenderBlockNode extends FunctionExpression +final class RenderBlockNode extends FunctionExpression { - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); $arguments = iterator_to_array($this->getNode('arguments')); diff --git a/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php b/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php index bf22c329d6a13..9967639d16636 100644 --- a/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php +++ b/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php @@ -18,15 +18,10 @@ /** * @author Bernhard Schussek - * - * @final since Symfony 4.4 */ -class SearchAndRenderBlockNode extends FunctionExpression +final class SearchAndRenderBlockNode extends FunctionExpression { - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); $compiler->raw('$this->env->getRuntime(\'Symfony\Component\Form\FormRenderer\')->searchAndRenderBlock('); diff --git a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php index b4dd8a9b37b4f..cfa4d8a197f9b 100644 --- a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php +++ b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php @@ -19,20 +19,15 @@ * Represents a stopwatch node. * * @author Wouter J - * - * @final since Symfony 4.4 */ -class StopwatchNode extends Node +final class StopwatchNode extends Node { public function __construct(Node $name, Node $body, AssignNameExpression $var, int $lineno = 0, string $tag = null) { parent::__construct(['body' => $body, 'name' => $name, 'var' => $var], [], $lineno, $tag); } - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) diff --git a/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php b/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php index 49ceac1404c5e..df29f0a19931f 100644 --- a/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php @@ -17,20 +17,15 @@ /** * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class TransDefaultDomainNode extends Node +final class TransDefaultDomainNode extends Node { public function __construct(AbstractExpression $expr, int $lineno = 0, string $tag = null) { parent::__construct(['expr' => $expr], [], $lineno, $tag); } - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { // noop as this node is just a marker for TranslationDefaultDomainNodeVisitor } diff --git a/src/Symfony/Bridge/Twig/Node/TransNode.php b/src/Symfony/Bridge/Twig/Node/TransNode.php index bc87d75bd7db2..8a126ba569172 100644 --- a/src/Symfony/Bridge/Twig/Node/TransNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransNode.php @@ -19,15 +19,10 @@ use Twig\Node\Node; use Twig\Node\TextNode; -// BC/FC with namespaced Twig -class_exists(ArrayExpression::class); - /** * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class TransNode extends Node +final class TransNode extends Node { public function __construct(Node $body, Node $domain = null, AbstractExpression $count = null, AbstractExpression $vars = null, AbstractExpression $locale = null, int $lineno = 0, string $tag = null) { @@ -48,10 +43,7 @@ public function __construct(Node $body, Node $domain = null, AbstractExpression parent::__construct($nodes, [], $lineno, $tag); } - /** - * @return void - */ - public function compile(Compiler $compiler) + public function compile(Compiler $compiler): void { $compiler->addDebugInfo($this); @@ -108,7 +100,7 @@ public function compile(Compiler $compiler) $compiler->raw(");\n"); } - protected function compileString(Node $body, ArrayExpression $vars, $ignoreStrictCheck = false) + private function compileString(Node $body, ArrayExpression $vars, bool $ignoreStrictCheck = false): array { if ($body instanceof ConstantExpression) { $msg = $body->getAttribute('value'); diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php index 642623f2a693c..efa354d03feac 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/Scope.php @@ -16,9 +16,9 @@ */ class Scope { - private $parent; - private $data = []; - private $left = false; + private ?self $parent; + private array $data = []; + private bool $left = false; public function __construct(self $parent = null) { @@ -27,20 +27,16 @@ public function __construct(self $parent = null) /** * Opens a new child scope. - * - * @return self */ - public function enter() + public function enter(): self { return new self($this); } /** * Closes current scope and returns parent one. - * - * @return self|null */ - public function leave() + public function leave(): ?self { $this->left = true; @@ -50,14 +46,11 @@ public function leave() /** * Stores data into current scope. * - * @param string $key - * @param mixed $value - * * @return $this * * @throws \LogicException */ - public function set($key, $value) + public function set(string $key, mixed $value): static { if ($this->left) { throw new \LogicException('Left scope is not mutable.'); @@ -70,12 +63,8 @@ public function set($key, $value) /** * Tests if a data is visible from current scope. - * - * @param string $key - * - * @return bool */ - public function has($key) + public function has(string $key): bool { if (\array_key_exists($key, $this->data)) { return true; @@ -90,13 +79,8 @@ public function has($key) /** * Returns data visible from current scope. - * - * @param string $key - * @param mixed $default - * - * @return mixed */ - public function get($key, $default = null) + public function get(string $key, mixed $default = null): mixed { if (\array_key_exists($key, $this->data)) { return $this->data[$key]; diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php index 72badea2d2bd0..213365ed9f1ef 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php @@ -27,10 +27,8 @@ /** * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor +final class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor { private $scope; @@ -41,10 +39,8 @@ public function __construct() /** * {@inheritdoc} - * - * @return Node */ - protected function doEnterNode(Node $node, Environment $env) + protected function doEnterNode(Node $node, Environment $env): Node { if ($node instanceof BlockNode || $node instanceof ModuleNode) { $this->scope = $this->scope->enter(); @@ -68,21 +64,18 @@ protected function doEnterNode(Node $node, Environment $env) return $node; } - if ($node instanceof FilterExpression && \in_array($node->getNode('filter')->getAttribute('value'), ['trans', 'transchoice'])) { + if ($node instanceof FilterExpression && 'trans' === $node->getNode('filter')->getAttribute('value')) { $arguments = $node->getNode('arguments'); - $ind = 'trans' === $node->getNode('filter')->getAttribute('value') ? 1 : 2; if ($this->isNamedArguments($arguments)) { - if (!$arguments->hasNode('domain') && !$arguments->hasNode($ind)) { + if (!$arguments->hasNode('domain') && !$arguments->hasNode(1)) { $arguments->setNode('domain', $this->scope->get('domain')); } - } else { - if (!$arguments->hasNode($ind)) { - if (!$arguments->hasNode($ind - 1)) { - $arguments->setNode($ind - 1, new ArrayExpression([], $node->getTemplateLine())); - } - - $arguments->setNode($ind, $this->scope->get('domain')); + } elseif (!$arguments->hasNode(1)) { + if (!$arguments->hasNode(0)) { + $arguments->setNode(0, new ArrayExpression([], $node->getTemplateLine())); } + + $arguments->setNode(1, $this->scope->get('domain')); } } elseif ($node instanceof TransNode) { if (!$node->hasNode('domain')) { @@ -95,10 +88,8 @@ protected function doEnterNode(Node $node, Environment $env) /** * {@inheritdoc} - * - * @return Node|null */ - protected function doLeaveNode(Node $node, Environment $env) + protected function doLeaveNode(Node $node, Environment $env): ?Node { if ($node instanceof TransDefaultDomainNode) { return null; @@ -113,10 +104,8 @@ protected function doLeaveNode(Node $node, Environment $env) /** * {@inheritdoc} - * - * @return int */ - public function getPriority() + public function getPriority(): int { return -10; } diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php index b9b5959bbd766..c8bee150982df 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php @@ -13,8 +13,10 @@ use Symfony\Bridge\Twig\Node\TransNode; use Twig\Environment; +use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; +use Twig\Node\Expression\FunctionExpression; use Twig\Node\Node; use Twig\NodeVisitor\AbstractNodeVisitor; @@ -22,45 +24,35 @@ * TranslationNodeVisitor extracts translation messages. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class TranslationNodeVisitor extends AbstractNodeVisitor +final class TranslationNodeVisitor extends AbstractNodeVisitor { public const UNDEFINED_DOMAIN = '_undefined'; - private $enabled = false; - private $messages = []; + private bool $enabled = false; + private array $messages = []; - /** - * @return void - */ - public function enable() + public function enable(): void { $this->enabled = true; $this->messages = []; } - /** - * @return void - */ - public function disable() + public function disable(): void { $this->enabled = false; $this->messages = []; } - public function getMessages() + public function getMessages(): array { return $this->messages; } /** * {@inheritdoc} - * - * @return Node */ - protected function doEnterNode(Node $node, Environment $env) + protected function doEnterNode(Node $node, Environment $env): Node { if (!$this->enabled) { return $node; @@ -77,21 +69,33 @@ protected function doEnterNode(Node $node, Environment $env) $this->getReadDomainFromArguments($node->getNode('arguments'), 1), ]; } elseif ( - $node instanceof FilterExpression && - 'transchoice' === $node->getNode('filter')->getAttribute('value') && - $node->getNode('node') instanceof ConstantExpression + $node instanceof FunctionExpression && + 't' === $node->getAttribute('name') ) { - // extract constant nodes with a trans filter - $this->messages[] = [ - $node->getNode('node')->getAttribute('value'), - $this->getReadDomainFromArguments($node->getNode('arguments'), 2), - ]; + $nodeArguments = $node->getNode('arguments'); + + if ($nodeArguments->getIterator()->current() instanceof ConstantExpression) { + $this->messages[] = [ + $this->getReadMessageFromArguments($nodeArguments, 0), + $this->getReadDomainFromArguments($nodeArguments, 2), + ]; + } } elseif ($node instanceof TransNode) { // extract trans nodes $this->messages[] = [ $node->getNode('body')->getAttribute('data'), $node->hasNode('domain') ? $this->getReadDomainFromNode($node->getNode('domain')) : null, ]; + } elseif ( + $node instanceof FilterExpression && + 'trans' === $node->getNode('filter')->getAttribute('value') && + $node->getNode('node') instanceof ConcatBinary && + $message = $this->getConcatValueFromNode($node->getNode('node'), null) + ) { + $this->messages[] = [ + $message, + $this->getReadDomainFromArguments($node->getNode('arguments'), 1), + ]; } return $node; @@ -99,24 +103,42 @@ protected function doEnterNode(Node $node, Environment $env) /** * {@inheritdoc} - * - * @return Node|null */ - protected function doLeaveNode(Node $node, Environment $env) + protected function doLeaveNode(Node $node, Environment $env): ?Node { return $node; } /** * {@inheritdoc} - * - * @return int */ - public function getPriority() + public function getPriority(): int { return 0; } + private function getReadMessageFromArguments(Node $arguments, int $index): ?string + { + if ($arguments->hasNode('message')) { + $argument = $arguments->getNode('message'); + } elseif ($arguments->hasNode($index)) { + $argument = $arguments->getNode($index); + } else { + return null; + } + + return $this->getReadMessageFromNode($argument); + } + + private function getReadMessageFromNode(Node $node): ?string + { + if ($node instanceof ConstantExpression) { + return $node->getAttribute('value'); + } + + return null; + } + private function getReadDomainFromArguments(Node $arguments, int $index): ?string { if ($arguments->hasNode('domain')) { @@ -138,4 +160,28 @@ private function getReadDomainFromNode(Node $node): ?string return self::UNDEFINED_DOMAIN; } + + private function getConcatValueFromNode(Node $node, ?string $value): ?string + { + if ($node instanceof ConcatBinary) { + foreach ($node as $nextNode) { + if ($nextNode instanceof ConcatBinary) { + $nextValue = $this->getConcatValueFromNode($nextNode, $value); + if (null === $nextValue) { + return null; + } + $value .= $nextValue; + } elseif ($nextNode instanceof ConstantExpression) { + $value .= $nextNode->getAttribute('value'); + } else { + // this is a node we cannot process (variable, or translation in translation) + return null; + } + } + } elseif ($node instanceof ConstantExpression) { + $value .= $node->getAttribute('value'); + } + + return $value; + } } diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig index 2f3a346df5903..0a52d36b374ed 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig @@ -16,7 +16,7 @@ {% block lead %} - {{ importance|upper }} + {% if importance is not null %}{{ importance|upper }}{% endif %}

{{ email.subject }}

@@ -49,13 +49,15 @@ {% block footer %} + {% if footer_text is defined and footer_text is not null %} {% block footer_content %} -

Notification e-mail sent by Symfony

+

{{ footer_text }}

{% endblock %}
+ {% endif %} {% endblock %}
diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig index db855829703e4..c98bb08a74c03 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig @@ -8,7 +8,7 @@ {% block action %} {% if action_url %} -{{ action_url }}: {{ action_text }} +{{ action_text }}: {{ action_url }} {% endif %} {% endblock %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index 12d9545f3aaff..34cbc76074acd 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -54,6 +54,11 @@ {%- endif -%} {%- endblock radio_widget %} +{% block choice_widget_collapsed -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} + {{- parent() -}} +{%- endblock choice_widget_collapsed %} + {# Labels #} {% block form_label -%} @@ -96,7 +101,7 @@ {%- endif -%} {%- endif -%} - {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain)) -}} + {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? (label_html is same as(false) ? label : label|raw) : (label_html is same as(false) ? label|trans(label_translation_parameters, translation_domain) : label|trans(label_translation_parameters, translation_domain)|raw)) -}} {%- endif -%} {%- endblock checkbox_radio_label %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index f08fc8d20f9cc..0e80840541fa1 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -121,6 +121,9 @@ {% block file_widget -%} <{{ element|default('div') }} class="custom-file"> {%- set type = type|default('file') -%} + {%- set input_lang = 'en' -%} + {% if app is defined and app.request is defined %}{%- set input_lang = app.request.locale -%}{%- endif -%} + {%- set attr = {lang: input_lang} | merge(attr) -%} {{- block('form_widget_simple') -}} {%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim })|filter((value, key) => key != 'id') -%} + {%- endif -%} +{%- endblock checkbox_radio_label %} + +{# Rows #} + +{%- block form_row -%} + {%- if compound is defined and compound -%} + {%- set element = 'fieldset' -%} + {%- endif -%} + {%- set widget_attr = {} -%} + {%- if help is not empty -%} + {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} + {%- endif -%} + {%- set row_class = row_class|default(row_attr.class|default('mb-3')|trim) -%} + <{{ element|default('div') }}{% with {attr: row_attr|merge({class: row_class})} %}{{ block('attributes') }}{% endwith %}> + {%- if 'form-floating' in row_class -%} + {{- form_widget(form, widget_attr) -}} + {{- form_label(form) -}} + {%- else -%} + {{- form_label(form) -}} + {{- form_widget(form, widget_attr) -}} + {%- endif -%} + {{- form_help(form) -}} + {{- form_errors(form) -}} + +{%- endblock form_row %} + +{%- block button_row -%} + + {{- form_widget(form) -}} + +{%- endblock button_row %} + +{# Errors #} + +{%- block form_errors -%} + {%- if errors|length > 0 -%} + {%- for error in errors -%} +
{{ error.message }}
+ {%- endfor -%} + {%- endif %} +{%- endblock form_errors %} + +{# Help #} + +{%- block form_help -%} + {% set row_class = row_attr.class|default('') %} + {% set help_class = ' form-text' %} + {% if 'input-group' in row_class %} + {#- Hack to properly display help with input group -#} + {% set help_class = ' input-group-text' %} + {% endif %} + {%- if help is not empty -%} + {%- set help_attr = help_attr|merge({class: (help_attr.class|default('') ~ help_class ~ ' mb-0')|trim}) -%} + {%- endif -%} + {{- parent() -}} +{%- endblock form_help %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig index 1b0092859dbd9..b8cb8c44aa832 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig @@ -143,11 +143,6 @@ {%- endif -%} {%- endblock dateinterval_widget -%} -{% block choice_widget_collapsed -%} - {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} - {{- parent() -}} -{%- endblock choice_widget_collapsed %} - {% block choice_widget_expanded -%} {%- if '-inline' in label_attr.class|default('') -%} {%- for child in form %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 066ed89c6372b..94f87dc165ec6 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -85,7 +85,7 @@ {{- block('choice_widget_options') -}} {%- else -%} - + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} @@ -232,7 +232,21 @@ {% set label = name|humanize %} {%- endif -%} {%- endif -%} - + {%- endblock button_widget -%} {%- block submit_widget -%} @@ -288,9 +302,17 @@ {%- endif -%} <{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}> {%- if translation_domain is same as(false) -%} - {{- label -}} + {%- if label_html is same as(false) -%} + {{- label -}} + {%- else -%} + {{- label|raw -}} + {%- endif -%} {%- else -%} - {{- label|trans(label_translation_parameters, translation_domain) -}} + {%- if label_html is same as(false) -%} + {{- label|trans(label_translation_parameters, translation_domain) -}} + {%- else -%} + {{- label|trans(label_translation_parameters, translation_domain)|raw -}} + {%- endif -%} {%- endif -%} {%- endif -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_6_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_6_layout.html.twig new file mode 100644 index 0000000000000..04ed730f5c799 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_6_layout.html.twig @@ -0,0 +1,50 @@ +{% extends "form_div_layout.html.twig" %} + +{%- block checkbox_row -%} + {%- set parent_class = parent_class|default(attr.class|default('')) -%} + {%- if 'switch-input' in parent_class -%} + {{- form_label(form) -}} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' switch-input')|trim}) -%} + {{- form_widget(form) -}} + + {{- form_errors(form) -}} + {%- else -%} + {{- block('form_row') -}} + {%- endif -%} +{%- endblock checkbox_row -%} + +{% block money_widget -%} + {% set prepend = not (money_pattern starts with '{{') %} + {% set append = not (money_pattern ends with '}}') %} + {% if prepend or append %} +
+ {% if prepend %} + {{ money_pattern|form_encode_currency }} + {% endif %} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' input-group-field')|trim}) %} + {{- block('form_widget_simple') -}} + {% if append %} + {{ money_pattern|form_encode_currency }} + {% endif %} +
+ {% else %} + {{- block('form_widget_simple') -}} + {% endif %} +{%- endblock money_widget %} + +{% block percent_widget -%} + {%- if symbol -%} +
+ {% set attr = attr|merge({class: (attr.class|default('') ~ ' input-group-field')|trim}) %} + {{- block('form_widget_simple') -}} + {{ symbol|default('%') }} +
+ {%- else -%} + {{- block('form_widget_simple') -}} + {%- endif -%} +{%- endblock percent_widget %} + +{% block button_widget -%} + {% set attr = attr|merge({class: (attr.class|default('') ~ ' button')|trim}) %} + {{- parent() -}} +{%- endblock button_widget %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig new file mode 100644 index 0000000000000..7f31e70b796c0 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/tailwind_2_layout.html.twig @@ -0,0 +1,69 @@ +{% use 'form_div_layout.html.twig' %} + +{%- block form_row -%} + {%- set row_attr = row_attr|merge({ class: row_attr.class|default(row_class|default('mb-6')) }) -%} + {{- parent() -}} +{%- endblock form_row -%} + +{%- block widget_attributes -%} + {%- set attr = attr|merge({ class: attr.class|default(widget_class|default('mt-1 w-full')) ~ (disabled ? ' ' ~ widget_disabled_class|default('border-gray-300 text-gray-500')) ~ (errors|length ? ' ' ~ widget_errors_class|default('border-red-700')) }) -%} + {{- parent() -}} +{%- endblock widget_attributes -%} + +{%- block form_label -%} + {%- set label_attr = label_attr|merge({ class: label_attr.class|default(label_class|default('block text-gray-800')) }) -%} + {{- parent() -}} +{%- endblock form_label -%} + +{%- block form_help -%} + {%- set help_attr = help_attr|merge({ class: help_attr.class|default(help_class|default('mt-1 text-gray-600')) }) -%} + {{- parent() -}} +{%- endblock form_help -%} + +{%- block form_errors -%} + {%- if errors|length > 0 -%} +
    + {%- for error in errors -%} +
  • {{ error.message }}
  • + {%- endfor -%} +
+ {%- endif -%} +{%- endblock form_errors -%} + +{%- block choice_widget_expanded -%} + {%- set attr = attr|merge({ class: attr.class|default('mt-2') }) -%} +
+ {%- for child in form %} +
+ {{- form_widget(child) -}} + {{- form_label(child, null, { translation_domain: choice_translation_domain }) -}} +
+ {% endfor -%} +
+{%- endblock choice_widget_expanded -%} + +{%- block checkbox_row -%} + {%- set row_attr = row_attr|merge({ class: row_attr.class|default(row_class|default('mb-6')) }) -%} + {%- set widget_attr = {} -%} + {%- if help is not empty -%} + {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} + {%- endif -%} + + {{- form_errors(form) -}} +
+ {{- form_widget(form, widget_attr) -}} + {{- form_label(form) -}} +
+ {{- form_help(form) -}} + +{%- endblock checkbox_row -%} + +{%- block checkbox_widget -%} + {%- set widget_class = widget_class|default('mr-2') -%} + {{- parent() -}} +{%- endblock checkbox_widget -%} + +{%- block radio_widget -%} + {%- set widget_class = widget_class|default('mr-2') -%} + {{- parent() -}} +{%- endblock radio_widget -%} diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php index 58c3cfde4beab..462f9f7874879 100644 --- a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -4,12 +4,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\AppVariable; -use Symfony\Bridge\Twig\Tests\Fixtures\TokenInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; class AppVariableTest extends TestCase @@ -95,13 +95,6 @@ public function testGetUser() $this->assertEquals($user, $this->appVariable->getUser()); } - public function testGetUserWithUsernameAsTokenUser() - { - $this->setTokenStorage($user = 'username'); - - $this->assertNull($this->appVariable->getUser()); - } - public function testGetTokenWithNoToken() { $tokenStorage = $this->createMock(TokenStorageInterface::class); diff --git a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php index 9fa1af4759eab..2488a27677af9 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Command\DebugCommand; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Twig\Environment; use Twig\Loader\ChainLoader; @@ -64,32 +65,6 @@ public function testWarningsWrongBundleOverriding() $this->assertEquals($expected, json_decode($tester->getDisplay(true), true)); } - /** - * @group legacy - * @expectedDeprecation Loading Twig templates from the "%sResources/BarBundle/views" directory is deprecated since Symfony 4.2, use "%stemplates/bundles/BarBundle" instead. - */ - public function testDeprecationForWrongBundleOverridingInLegacyPath() - { - $bundleMetadata = [ - 'TwigBundle' => 'vendor/twig-bundle/', - 'WebProfilerBundle' => 'vendor/web-profiler-bundle/', - ]; - $defaultPath = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'templates'; - $rootDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'; - - $tester = $this->createCommandTester([], $bundleMetadata, $defaultPath, $rootDir); - $ret = $tester->execute(['--filter' => 'unknown', '--format' => 'json'], ['decorated' => false]); - - $expected = ['warnings' => [ - 'Path "Resources/BarBundle" not matching any bundle found', - 'Path "templates/bundles/UnknownBundle" not matching any bundle found', - 'Path "templates/bundles/WebProfileBundle" not matching any bundle found, did you mean "WebProfilerBundle"?', - ]]; - - $this->assertEquals(0, $ret, 'Returns 0 in case of success'); - $this->assertEquals($expected, json_decode($tester->getDisplay(true), true)); - } - public function testMalformedTemplateName() { $this->expectException(InvalidArgumentException::class); @@ -281,7 +256,7 @@ public function getDebugTemplateNameTestData() public function testDebugTemplateNameWithChainLoader() { - $tester = $this->createCommandTester(['templates/' => null], [], null, null, true); + $tester = $this->createCommandTester(['templates/' => null], [], null, true); $ret = $tester->execute(['name' => 'base.html.twig'], ['decorated' => false]); $this->assertEquals(0, $ret, 'Returns 0 in case of success'); @@ -291,7 +266,7 @@ public function testDebugTemplateNameWithChainLoader() public function testWithGlobals() { $message = 'foo'; - $tester = $this->createCommandTester([], [], null, null, false, ['message' => $message]); + $tester = $this->createCommandTester([], [], null, false, ['message' => $message]); $tester->execute([], ['decorated' => true]); $display = $tester->getDisplay(); $this->assertStringContainsString(json_encode($message), $display); @@ -300,7 +275,7 @@ public function testWithGlobals() public function testWithGlobalsJson() { $globals = ['message' => 'foo']; - $tester = $this->createCommandTester([], [], null, null, false, $globals); + $tester = $this->createCommandTester([], [], null, false, $globals); $tester->execute(['--format' => 'json'], ['decorated' => true]); $display = $tester->getDisplay(); $display = json_decode($display, true); @@ -319,7 +294,34 @@ public function testWithFilter() $this->assertNotSame($display1, $display2); } - private function createCommandTester(array $paths = [], array $bundleMetadata = [], string $defaultPath = null, string $rootDir = null, bool $useChainLoader = false, array $globals = []): CommandTester + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $projectDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'; + $loader = new FilesystemLoader([], $projectDir); + $environment = new Environment($loader); + + $application = new Application(); + $application->add(new DebugCommand($environment, $projectDir, [], null, null)); + + $tester = new CommandCompletionTester($application->find('debug:twig')); + $suggestions = $tester->complete($input, 2); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'name' => [['email'], []]; + yield 'option --format' => [['--format', ''], ['text', 'json']]; + } + + private function createCommandTester(array $paths = [], array $bundleMetadata = [], string $defaultPath = null, bool $useChainLoader = false, array $globals = []): CommandTester { $projectDir = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Fixtures'; $loader = new FilesystemLoader([], $projectDir); @@ -341,7 +343,7 @@ private function createCommandTester(array $paths = [], array $bundleMetadata = } $application = new Application(); - $application->add(new DebugCommand($environment, $projectDir, $bundleMetadata, $defaultPath, null, $rootDir)); + $application->add(new DebugCommand($environment, $projectDir, $bundleMetadata, $defaultPath, null)); $command = $application->find('debug:twig'); return new CommandTester($command); diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 9bb9a9867c745..6a3d640b2d5b2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -14,7 +14,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Command\LintCommand; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Twig\Environment; use Twig\Loader\FilesystemLoader; @@ -107,7 +109,58 @@ public function testLintDefaultPaths() self::assertStringContainsString('OK in', trim($tester->getDisplay())); } + public function testLintIncorrectFileWithGithubFormat() + { + $filename = $this->createFile('{{ foo'); + $tester = $this->createCommandTester(); + $tester->execute(['filename' => [$filename], '--format' => 'github'], ['decorated' => false]); + self::assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); + self::assertStringMatchesFormat('%A::error file=%s,line=1,col=0::Unexpected token "end of template" ("end of print statement" expected).%A', trim($tester->getDisplay())); + } + + public function testLintAutodetectsGithubActionEnvironment() + { + $prev = getenv('GITHUB_ACTIONS'); + putenv('GITHUB_ACTIONS'); + + try { + putenv('GITHUB_ACTIONS=1'); + + $filename = $this->createFile('{{ foo'); + $tester = $this->createCommandTester(); + + $tester->execute(['filename' => [$filename]], ['decorated' => false]); + self::assertStringMatchesFormat('%A::error file=%s,line=1,col=0::Unexpected token "end of template" ("end of print statement" expected).%A', trim($tester->getDisplay())); + } finally { + putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : '')); + } + } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $tester = new CommandCompletionTester($this->createCommand()); + + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions() + { + yield 'option' => [['--format', ''], ['txt', 'json', 'github']]; + } + private function createCommandTester(): CommandTester + { + return new CommandTester($this->createCommand()); + } + + private function createCommand(): Command { $environment = new Environment(new FilesystemLoader(\dirname(__DIR__).'/Fixtures/templates/')); $environment->addFilter(new TwigFilter('deprecated_filter', function ($v) { @@ -118,9 +171,8 @@ private function createCommandTester(): CommandTester $application = new Application(); $application->add($command); - $command = $application->find('lint:twig'); - return new CommandTester($command); + return $application->find('lint:twig'); } private function createFile($content): string diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php index 69064a003d7fe..3929877438132 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php @@ -100,6 +100,39 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ); } + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-sm-2 control-label required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-sm-2 control-label required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-sm-2 control-label required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-sm-2 control-label required"]/b[.="Bolded label"]'); + } + public function testStartTag() { $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php index eb40559ef6b55..6e08f650bb963 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php @@ -104,6 +104,39 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ); } + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class control-label required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class control-label required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class control-label required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class control-label required"]/b[.="Bolded label"]'); + } + public function testHelp() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ @@ -1729,28 +1762,9 @@ public function testDateTimeWithWidgetSingleText() ); } - /** - * @group legacy - */ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateTimeType', '2011-02-03 04:05:06', [ - 'input' => 'string', - 'date_widget' => 'choice', - 'time_widget' => 'choice', - 'widget' => 'single_text', - 'model_timezone' => 'UTC', - 'view_timezone' => 'UTC', - ]); - - $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input - [@type="datetime-local"] - [@name="name"] - [@class="my&class form-control"] - [@value="2011-02-03T04:05:06"] -' - ); + $this->markTestSkipped('Make tests pass with symfony/form 4.4'); } public function testDateChoice() @@ -2195,7 +2209,7 @@ public function testPasswordWithMaxLength() public function testPercent() { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PercentType', 0.1); + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PercentType', 0.1, ['rounding_mode' => \NumberFormatter::ROUND_CEILING]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/div @@ -2217,7 +2231,7 @@ public function testPercent() public function testPercentNoSymbol() { - $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false]); + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false, 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/input [@id="my&id"] @@ -2231,7 +2245,7 @@ public function testPercentNoSymbol() public function testPercentCustomSymbol() { - $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => '‱']); + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => '‱', 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/div [@class="input-group"] @@ -2680,6 +2694,31 @@ public function testButtonlabelWithoutTranslation() ); } + public function testButtonLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, [ + 'label' => 'Click here!', + ]); + + $html = $this->renderWidget($form->createView(), ['attr' => ['class' => 'my&class']]); + + $this->assertMatchesXpath($html, '/button[@type="button"][@name="name"][.="[trans]Click here![/trans]"][@class="my&class btn"]'); + $this->assertMatchesXpath($html, '/button[@type="button"][@name="name"][@class="my&class btn"]/b[.="Click here!"]', 0); + } + + public function testButtonLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ButtonType', null, [ + 'label' => 'Click here!', + 'label_html' => true, + ]); + + $html = $this->renderWidget($form->createView(), ['attr' => ['class' => 'my&class']]); + + $this->assertMatchesXpath($html, '/button[@type="button"][@name="name"][.="[trans]Click here![/trans]"][@class="my&class btn"]', 0); + $this->assertMatchesXpath($html, '/button[@type="button"][@name="name"][@class="my&class btn"]/b[.="Click here!"]'); + } + public function testSubmit() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\SubmitType'); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php index e20f4818b2810..39d49d97a0bd6 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php @@ -132,6 +132,39 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ); } + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"]/b[.="Bolded label"]'); + } + public function testLegendOnExpandedType() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', null, [ diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php index 224469a516f2e..61006607c33e0 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php @@ -142,6 +142,39 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ); } + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"]/b[.="Bolded label"]'); + } + public function testLegendOnExpandedType() { $form = $this->factory->createNamed('name', ChoiceType::class, null, [ @@ -1154,7 +1187,7 @@ public function testMoney() public function testPercent() { - $form = $this->factory->createNamed('name', PercentType::class, 0.1); + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['rounding_mode' => \NumberFormatter::ROUND_CEILING]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/div @@ -1180,7 +1213,7 @@ public function testPercent() public function testPercentNoSymbol() { - $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false]); + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false, 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/input [@id="my&id"] @@ -1194,7 +1227,7 @@ public function testPercentNoSymbol() public function testPercentCustomSymbol() { - $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => '‱']); + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => '‱', 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], '/div [@class="input-group"] diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5HorizontalLayoutTest.php new file mode 100644 index 0000000000000..9fe231bfa1198 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5HorizontalLayoutTest.php @@ -0,0 +1,353 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\RadioType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormError; + +/** + * Abstract class providing test cases for the Bootstrap 5 horizontal Twig form theme. + * + * @author Romain Monteil + */ +abstract class AbstractBootstrap5HorizontalLayoutTest extends AbstractBootstrap5LayoutTest +{ + public function testRow() + { + $form = $this->factory->createNamed('name', TextType::class); + $form->addError(new FormError('[trans]Error![/trans]')); + $html = $this->renderRow($form->createView()); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-3 row"] + [ + ./label + [@for="name"] + [@class="col-form-label col-sm-2 required"] + /following-sibling::div + [@class="col-sm-10"] + [ + ./input[@id="name"] + /following-sibling::div + [@class="invalid-feedback d-block"] + [.="[trans]Error![/trans]"] + ] + [count(./div)=1] + ] +' + ); + } + + public function testRowWithCustomClass() + { + $form = $this->factory->createNamed('name', TextType::class); + $form->addError(new FormError('[trans]Error![/trans]')); + $html = $this->renderRow($form->createView(), [ + 'row_attr' => [ + 'class' => 'mb-5', + ], + ]); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-5 row"] + [ + ./label + [@for="name"] + [@class="col-form-label col-sm-2 required"] + /following-sibling::div + [@class="col-sm-10"] + [ + ./input[@id="name"] + /following-sibling::div + [@class="invalid-feedback d-block"] + [.="[trans]Error![/trans]"] + ] + [count(./div)=1] + ] +' + ); + } + + public function testLabelOnForm() + { + $form = $this->factory->createNamed('name', DateType::class); + $view = $form->createView(); + $this->renderWidget($view, ['label' => 'foo']); + $html = $this->renderLabel($view); + + $this->assertMatchesXpath($html, + '/legend + [@class="col-form-label col-sm-2 required"] + [.="[trans]Name[/trans]"] +' + ); + } + + public function testLabelDoesNotRenderFieldAttributes() + { + $form = $this->factory->createNamed('name', TextType::class); + $html = $this->renderLabel($form->createView(), null, [ + 'attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="col-form-label col-sm-2 required"] +' + ); + } + + public function testLabelWithCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', TextType::class); + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class col-form-label col-sm-2 required"] +' + ); + } + + public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', TextType::class); + $html = $this->renderLabel($form->createView(), 'Custom label', [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class col-form-label col-sm-2 required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'label' => 'Custom label', + ]); + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, +'/label + [@for="name"] + [@class="my&class col-form-label col-sm-2 required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class col-form-label col-sm-2 required"]/b[.="Bolded label"]'); + } + + public function testLegendOnExpandedType() + { + $form = $this->factory->createNamed('name', ChoiceType::class, null, [ + 'label' => 'Custom label', + 'expanded' => true, + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + ]); + $view = $form->createView(); + $this->renderWidget($view); + $html = $this->renderLabel($view); + + $this->assertMatchesXpath($html, +'/legend + [@class="col-sm-2 col-form-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testCheckboxRow() + { + $form = $this->factory->createNamed('name', CheckboxType::class); + $view = $form->createView(); + $html = $this->renderRow($view, ['label' => 'foo']); + + $this->assertMatchesXpath($html, '/div[@class="mb-3 row"]/div[@class="col-sm-2" or @class="col-sm-10"]', 2); + } + + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', CheckboxType::class); + $view = $form->createView(); + $html = $this->renderRow($view, ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="mb-3 row"] + [ + ./div[@class="col-sm-2" or @class="col-sm-10"] + /following-sibling::div[@class="col-sm-2" or @class="col-sm-10"] + [ + ./p + [@class="form-text mb-0 help-text"] + [.="[trans]really helpful text[/trans]"] + ] + ] +' + ); + } + + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', RadioType::class, false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="mb-3 row"] + [ + ./div[@class="col-sm-2" or @class="col-sm-10"] + /following-sibling::div[@class="col-sm-2" or @class="col-sm-10"] + [ + ./p + [@class="form-text mb-0 help-text"] + [.="[trans]really helpful text[/trans]"] + ] + ] +' + ); + } + + public function testFileWithGroup() + { + $form = $this->factory->createNamed('name', FileType::class); + $html = $this->renderRow($form->createView(), [ + 'id' => 'n/a', + 'attr' => [ + 'class' => 'my&class', + ], + 'row_attr' => [ + 'class' => 'input-group mb-3', + ], + ]); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-3 row"] + [ + ./div + [@class="col-sm-2"] + /following-sibling::div + [@class="col-sm-10"] + [ + ./div + [@class="input-group"] + [ + ./label + [@class="input-group-text required"] + [.="[trans]Name[/trans]"] + /following-sibling::input + [@type="file"] + [@name="name"] + [@class="my&class form-control"] + ] + ] + ] +' + ); + } + + public function testFloatingLabel() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'attr' => [ + 'placeholder' => 'name', + ], + 'row_attr' => [ + 'class' => 'form-floating mb-3', + ], + ]); + + $html = $this->renderRow($form->createView()); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-3 row"] + [ + ./div + [@class="col-sm-2"] + /following-sibling::div + [@class="col-sm-10"] + [ + ./div + [@class="form-floating"] + [ + ./input + [@id="name"] + [@placeholder="[trans]name[/trans]"] + /following-sibling::label + [@for="name"] + ] + ] + ] +' + ); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php new file mode 100644 index 0000000000000..ff35789a564cd --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap5LayoutTest.php @@ -0,0 +1,1824 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Component\Form\Extension\Core\Type\BirthdayType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\ColorType; +use Symfony\Component\Form\Extension\Core\Type\CountryType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Form\Extension\Core\Type\LocaleType; +use Symfony\Component\Form\Extension\Core\Type\MoneyType; +use Symfony\Component\Form\Extension\Core\Type\PercentType; +use Symfony\Component\Form\Extension\Core\Type\RadioType; +use Symfony\Component\Form\Extension\Core\Type\RangeType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\TimeType; +use Symfony\Component\Form\Extension\Core\Type\TimezoneType; +use Symfony\Component\Form\Extension\Core\Type\WeekType; +use Symfony\Component\Form\FormError; + +/** + * Abstract class providing test cases for the Bootstrap 5 Twig form theme. + * + * @author Romain Monteil + */ +abstract class AbstractBootstrap5LayoutTest extends AbstractBootstrap4LayoutTest +{ + public function testRow() + { + $form = $this->factory->createNamed('name', TextType::class); + $form->addError(new FormError('[trans]Error![/trans]')); + $html = $this->renderRow($form->createView()); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-3"] + [ + ./label[@for="name"] + /following-sibling::input[@id="name"] + /following-sibling::div + [@class="invalid-feedback d-block"] + [.="[trans]Error![/trans]"] + ] + [count(./div)=1] +' + ); + } + + public function testRowWithCustomClass() + { + $form = $this->factory->createNamed('name', TextType::class); + $form->addError(new FormError('[trans]Error![/trans]')); + $html = $this->renderRow($form->createView(), [ + 'row_attr' => [ + 'class' => 'mb-5', + ], + ]); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-5"] + [ + ./label[@for="name"] + /following-sibling::input[@id="name"] + /following-sibling::div + [@class="invalid-feedback d-block"] + [.="[trans]Error![/trans]"] + ] + [count(./div)=1] +' + ); + } + + public function testLabelDoesNotRenderFieldAttributes() + { + $form = $this->factory->createNamed('name', TextType::class); + $html = $this->renderLabel($form->createView(), null, [ + 'attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, + '/label + [@for="name"] + [@class="form-label required"] +' + ); + } + + public function testLabelWithCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', TextType::class); + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, + '/label + [@for="name"] + [@class="my&class form-label required"] +' + ); + } + + public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', TextType::class); + $html = $this->renderLabel($form->createView(), 'Custom label', [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, + '/label + [@for="name"] + [@class="my&class form-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'label' => 'Custom label', + ]); + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, + '/label + [@for="name"] + [@class="my&class form-label required"] + [.="[trans]Custom label[/trans]"] +' + ); + } + + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class form-label required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class form-label required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class form-label required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class form-label required"]/b[.="Bolded label"]'); + } + + public function testHelp() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'help' => 'Help text test!', + ]); + $view = $form->createView(); + $html = $this->renderHelp($view); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + [.="[trans]Help text test![/trans]"] +' + ); + } + + public function testHelpAttr() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'help' => 'Help text test!', + 'help_attr' => [ + 'class' => 'class-test', + ], + ]); + $view = $form->createView(); + $html = $this->renderHelp($view); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="class-test form-text mb-0 help-text"] + [.="[trans]Help text test![/trans]"] +' + ); + } + + public function testHelpHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'help' => 'Help text test!', + ]); + + $view = $form->createView(); + $html = $this->renderHelp($view); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + [.="[trans]Help text test![/trans]"] +' + ); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + /b + [.="text"] +', 0 + ); + } + + public function testHelpHtmlIsFalse() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'help' => 'Help text test!', + 'help_html' => false, + ]); + + $view = $form->createView(); + $html = $this->renderHelp($view); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + [.="[trans]Help text test![/trans]"] +' + ); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + /b + [.="text"] +', 0 + ); + } + + public function testHelpHtmlIsTrue() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'help' => 'Help text test!', + 'help_html' => true, + ]); + $html = $this->renderHelp($form->createView()); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + [.="[trans]Help text test![/trans]"] +', 0 + ); + + $this->assertMatchesXpath($html, + '/p + [@id="name_help"] + [@class="form-text mb-0 help-text"] + /b + [.="text"] +' + ); + } + + public function testErrors() + { + $form = $this->factory->createNamed('name', TextType::class); + $form->addError(new FormError('[trans]Error 1[/trans]')); + $form->addError(new FormError('[trans]Error 2[/trans]')); + $html = $this->renderErrors($form->createView()); + + $this->assertMatchesXpath($html, + '/div + [@class="invalid-feedback d-block"] + [.="[trans]Error 1[/trans]"] + /following-sibling::div + [@class="invalid-feedback d-block"] + [.="[trans]Error 2[/trans]"] +' + ); + } + + public function testErrorWithNoLabel() + { + self::markTestSkipped('Errors are no longer rendered inside label with Bootstrap 5.'); + } + + public function testSingleChoiceAttributesWithMainAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'multiple' => false, + 'expanded' => false, + 'attr' => ['class' => 'bar&baz'], + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], + '/select + [@name="name"] + [@class="bar&baz form-select"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', CheckboxType::class); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-3"] + [ + ./div[@class="form-check"] + [ + ./input + [@type="checkbox"] + [@id="name"] + [@name="name"] + [@required="required"] + [@aria-describedby="name_help"] + [@class="form-check-input"] + [@value="1"] + /following-sibling::label + [@class="form-check-label required"] + [@for="name"] + [.="[trans]foo[/trans]"] + ] + /following-sibling::p + [@class="form-text mb-0 help-text"] + [.="[trans]really helpful text[/trans]"] + ] +' + ); + } + + public function testCheckboxSwitchWithValue() + { + $form = $this->factory->createNamed('name', CheckboxType::class, false, [ + 'value' => 'foo&bar', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class'], 'label_attr' => ['class' => 'checkbox-switch']], + '/div + [@class="form-check form-switch"] + [ + ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class form-check-input"][@value="foo&bar"] + /following-sibling::label + [@class="checkbox-switch form-check-label required"] + [.="[trans]Name[/trans]"] + ] +' + ); + } + + public function testMultipleChoiceSkipsPlaceholder() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'multiple' => true, + 'expanded' => false, + 'placeholder' => 'Test&Me', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name[]"] + [@class="my&class form-select"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleChoice() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleChoiceWithoutTranslation() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'multiple' => false, + 'expanded' => false, + 'choice_translation_domain' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="Choice&A"] + /following-sibling::option[@value="&b"][not(@selected)][.="Choice&B"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleChoiceWithPlaceholderWithoutTranslation() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'multiple' => false, + 'expanded' => false, + 'required' => false, + 'translation_domain' => false, + 'placeholder' => 'Placeholder&Not&Translated', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value=""][not(@selected)][not(@disabled)][.="Placeholder&Not&Translated"] + /following-sibling::option[@value="&a"][@selected="selected"][.="Choice&A"] + /following-sibling::option[@value="&b"][not(@selected)][.="Choice&B"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testSingleChoiceWithPreferred() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&b'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=4] +' + ); + } + + public function testSingleChoiceWithSelectedPreferred() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&a'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=4] +' + ); + } + + public function testSingleChoiceWithPreferredAndNoSeparator() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&b'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => null, 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceWithPreferredAndBlankSeparator() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&b'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.=""] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=4] +' + ); + } + + public function testChoiceWithOnlyPreferred() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&a', '&b'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@class="my&class form-select"] + [count(./option)=5] +' + ); + } + + public function testSingleChoiceNonRequired() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'required' => false, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value=""][.=""] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceNonRequiredNoneSelected() + { + $form = $this->factory->createNamed('name', ChoiceType::class, null, [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'required' => false, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value=""][.=""] + /following-sibling::option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceNonRequiredWithPlaceholder() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'multiple' => false, + 'expanded' => false, + 'required' => false, + 'placeholder' => 'Select&Anything&Not&Me', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [ + ./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Anything&Not&Me[/trans]"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceRequiredWithPlaceholder() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'required' => true, + 'multiple' => false, + 'expanded' => false, + 'placeholder' => 'Test&Me', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [@required="required"] + [ + ./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Test&Me[/trans]"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceRequiredWithPlaceholderViaView() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'required' => true, + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['placeholder' => '', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [@required="required"] + [ + ./option[@value=""][not(@selected)][not(@disabled)][.=""] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=3] +' + ); + } + + public function testSingleChoiceGrouped() + { + $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ + 'choices' => [ + 'Group&1' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'Group&2' => ['Choice&C' => '&c'], + ], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [./optgroup[@label="[trans]Group&1[/trans]"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] + ] + [./optgroup[@label="[trans]Group&2[/trans]"] + [./option[@value="&c"][not(@selected)][.="[trans]Choice&C[/trans]"]] + [count(./option)=1] + ] + [count(./optgroup)=2] +' + ); + } + + public function testMultipleChoice() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'required' => true, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name[]"] + [@class="my&class form-select"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testMultipleChoiceAttributes() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'choice_attr' => ['Choice&B' => ['class' => 'foo&bar']], + 'required' => true, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name[]"] + [@class="my&class form-select"] + [@required="required"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][@class="foo&bar"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testMultipleChoiceNonRequired() + { + $form = $this->factory->createNamed('name', ChoiceType::class, ['&a'], [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'required' => false, + 'multiple' => true, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name[]"] + [@class="my&class form-select"] + [@multiple="multiple"] + [ + ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=2] +' + ); + } + + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', RadioType::class, false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="mb-3"] + [ + ./p + [@class="form-text mb-0 help-text"] + [.="[trans]really helpful text[/trans]"] + ] +' + ); + } + + public function testFile() + { + $form = $this->factory->createNamed('name', FileType::class); + + $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'n/a', 'attr' => ['class' => 'my&class']], + '/input + [@type="file"] + [@name="name"] + [@class="my&class form-control"] +' + ); + } + + public function testFileLabelIdNotDuplicated() + { + $this->markTestSkipped('The Bootstrap 5 form theme does not use the file widget shipped with the Bootstrap 4 theme.'); + } + + public function testFileWithGroup() + { + $form = $this->factory->createNamed('name', FileType::class); + $html = $this->renderRow($form->createView(), [ + 'id' => 'n/a', + 'attr' => [ + 'class' => 'my&class', + ], + 'row_attr' => [ + 'class' => 'input-group mb-3', + ], + ]); + + $this->assertMatchesXpath($html, + '/div + [@class="input-group mb-3"] + [ + ./label + [@class="input-group-text required"] + [.="[trans]Name[/trans]"] + /following-sibling::input + [@type="file"] + [@name="name"] + [@class="my&class form-control"] + ] +' + ); + } + + public function testFileWithPlaceholder() + { + self::markTestSkipped('Placeholder does not apply on input file.'); + } + + public function testCountry() + { + $form = $this->factory->createNamed('name', CountryType::class, 'AT'); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [./option[@value="AT"][@selected="selected"][.="Austria"]] + [count(./option)>200] +' + ); + } + + public function testCountryWithPlaceholder() + { + $form = $this->factory->createNamed('name', CountryType::class, 'AT', [ + 'placeholder' => 'Select&Country', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Country[/trans]"]] + [./option[@value="AT"][@selected="selected"][.="Austria"]] + [count(./option)>201] +' + ); + } + + public function testDateTime() + { + $form = $this->factory->createNamed('name', DateTimeType::class, date('Y').'-02-03 04:05:06', [ + 'input' => 'string', + 'with_seconds' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + + ./select + [@id="name_date_month"] + [@class="form-select"] + [./option[@value="2"][@selected="selected"]] + /following-sibling::select + [@id="name_date_day"] + [@class="form-select"] + [./option[@value="3"][@selected="selected"]] + /following-sibling::select + [@id="name_date_year"] + [@class="form-select"] + [./option[@value="'.date('Y').'"][@selected="selected"]] + ] + /following-sibling::div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_time_hour"] + [@class="form-select"] + [./option[@value="4"][@selected="selected"]] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_time_minute"] + [@class="form-select"] + [./option[@value="5"][@selected="selected"]] + ] + ] + [count(.//select)=5] +' + ); + } + + public function testDateTimeWithPlaceholderGlobal() + { + $form = $this->factory->createNamed('name', DateTimeType::class, null, [ + 'input' => 'string', + 'placeholder' => 'Change&Me', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_date_month"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + /following-sibling::select + [@id="name_date_day"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + /following-sibling::select + [@id="name_date_year"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + ] + /following-sibling::div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_time_hour"] + [@class="form-select"] + [./option[@value=""][.="[trans]Change&Me[/trans]"]] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_time_minute"] + [@class="form-select"] + [./option[@value=""][.="[trans]Change&Me[/trans]"]] + ] + ] + [count(.//select)=5] +' + ); + } + + public function testDateTimeWithHourAndMinute() + { + $data = ['year' => date('Y'), 'month' => '2', 'day' => '3', 'hour' => '4', 'minute' => '5']; + + $form = $this->factory->createNamed('name', DateTimeType::class, $data, [ + 'input' => 'array', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_date_month"] + [@class="form-select"] + [./option[@value="2"][@selected="selected"]] + /following-sibling::select + [@id="name_date_day"] + [@class="form-select"] + [./option[@value="3"][@selected="selected"]] + /following-sibling::select + [@id="name_date_year"] + [@class="form-select"] + [./option[@value="'.date('Y').'"][@selected="selected"]] + ] + /following-sibling::div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_time_hour"] + [@class="form-select"] + [./option[@value="4"][@selected="selected"]] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_time_minute"] + [@class="form-select"] + [./option[@value="5"][@selected="selected"]] + ] + ] + [count(.//select)=5] +' + ); + } + + public function testDateTimeWithSeconds() + { + $form = $this->factory->createNamed('name', DateTimeType::class, date('Y').'-02-03 04:05:06', [ + 'input' => 'string', + 'with_seconds' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_date_month"] + [@class="form-select"] + [./option[@value="2"][@selected="selected"]] + /following-sibling::select + [@id="name_date_day"] + [@class="form-select"] + [./option[@value="3"][@selected="selected"]] + /following-sibling::select + [@id="name_date_year"] + [@class="form-select"] + [./option[@value="'.date('Y').'"][@selected="selected"]] + ] + /following-sibling::div + [@class="visually-hidden"] + /following-sibling::div + [@class="input-group"] + [ + ./select + [@id="name_time_hour"] + [@class="form-select"] + [./option[@value="4"][@selected="selected"]] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_time_minute"] + [@class="form-select"] + [./option[@value="5"][@selected="selected"]] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_time_second"] + [@class="form-select"] + [./option[@value="6"][@selected="selected"]] + ] + ] + [count(.//select)=6] +' + ); + } + + public function testDateTimeSingleText() + { + $form = $this->factory->createNamed('name', DateTimeType::class, '2011-02-03 04:05:06', [ + 'input' => 'string', + 'date_widget' => 'single_text', + 'time_widget' => 'single_text', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./input + [@type="date"] + [@id="name_date"] + [@name="name[date]"] + [@class="form-control"] + [@value="2011-02-03"] + /following-sibling::input + [@type="time"] + [@id="name_time"] + [@name="name[time]"] + [@class="form-control"] + [@value="04:05"] + ] +' + ); + } + + public function testDateChoice() + { + $form = $this->factory->createNamed('name', DateType::class, date('Y').'-02-03', [ + 'input' => 'string', + 'widget' => 'choice', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_month"] + [@class="form-select"] + [./option[@value="2"][@selected="selected"]] + /following-sibling::select + [@id="name_day"] + [@class="form-select"] + [./option[@value="3"][@selected="selected"]] + /following-sibling::select + [@id="name_year"] + [@class="form-select"] + [./option[@value="'.date('Y').'"][@selected="selected"]] + ] + [count(./select)=3] + ] +' + ); + } + + public function testDateChoiceWithPlaceholderGlobal() + { + $form = $this->factory->createNamed('name', DateType::class, null, [ + 'input' => 'string', + 'widget' => 'choice', + 'placeholder' => 'Change&Me', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_month"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + /following-sibling::select + [@id="name_day"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + /following-sibling::select + [@id="name_year"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + ] + [count(./select)=3] + ] +' + ); + } + + public function testDateChoiceWithPlaceholderOnYear() + { + $form = $this->factory->createNamed('name', DateType::class, null, [ + 'input' => 'string', + 'widget' => 'choice', + 'required' => false, + 'placeholder' => ['year' => 'Change&Me'], + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_month"] + [@class="form-select"] + [./option[@value="1"]] + /following-sibling::select + [@id="name_day"] + [@class="form-select"] + [./option[@value="1"]] + /following-sibling::select + [@id="name_year"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + ] + [count(./select)=3] + ] +' + ); + } + + public function testDateText() + { + $form = $this->factory->createNamed('name', DateType::class, '2011-02-03', [ + 'input' => 'string', + 'widget' => 'text', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./input + [@id="name_month"] + [@type="text"] + [@class="form-control"] + [@value="2"] + /following-sibling::input + [@id="name_day"] + [@type="text"] + [@class="form-control"] + [@value="3"] + /following-sibling::input + [@id="name_year"] + [@type="text"] + [@class="form-control"] + [@value="2011"] + ] + [count(./input)=3] + ] +' + ); + } + + public function testBirthDay() + { + $form = $this->factory->createNamed('name', BirthdayType::class, '2000-02-03', [ + 'input' => 'string', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_month"] + [@class="form-select"] + [./option[@value="2"][@selected="selected"]] + /following-sibling::select + [@id="name_day"] + [@class="form-select"] + [./option[@value="3"][@selected="selected"]] + /following-sibling::select + [@id="name_year"] + [@class="form-select"] + [./option[@value="2000"][@selected="selected"]] + ] + [count(./select)=3] + ] +' + ); + } + + public function testBirthDayWithPlaceholder() + { + $form = $this->factory->createNamed('name', BirthdayType::class, '1950-01-01', [ + 'input' => 'string', + 'placeholder' => '', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_month"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.=""]] + [./option[@value="1"][@selected="selected"]] + /following-sibling::select + [@id="name_day"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.=""]] + [./option[@value="1"][@selected="selected"]] + /following-sibling::select + [@id="name_year"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.=""]] + [./option[@value="1950"][@selected="selected"]] + ] + [count(./select)=3] + ] +' + ); + } + + public function testLanguage() + { + $form = $this->factory->createNamed('name', LanguageType::class, 'de'); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [./option[@value="de"][@selected="selected"][.="German"]] + [count(./option)>200] +' + ); + } + + public function testLocale() + { + $form = $this->factory->createNamed('name', LocaleType::class, 'de_AT'); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [./option[@value="de_AT"][@selected="selected"][.="German (Austria)"]] + [count(./option)>200] +' + ); + } + + public function testMoney() + { + $form = $this->factory->createNamed('name', MoneyType::class, 1234.56, [ + 'currency' => 'EUR', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], + '/div + [@class="input-group"] + [ + ./span + [@class="input-group-text"] + [contains(.., "€")] + /following-sibling::input + [@id="my&id"] + [@type="text"] + [@name="name"] + [@class="my&class form-control"] + [@value="1234.56"] + ] +' + ); + } + + public function testPercent() + { + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['rounding_mode' => \NumberFormatter::ROUND_CEILING]); + + $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], + '/div + [@class="input-group"] + [ + ./input + [@id="my&id"] + [@type="text"] + [@name="name"] + [@class="my&class form-control"] + [@value="10"] + /following-sibling::span + [@class="input-group-text"] + [contains(.., "%")] + ] +' + ); + } + + public function testPercentCustomSymbol() + { + $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => '‱', 'rounding_mode' => \NumberFormatter::ROUND_CEILING]); + $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], + '/div + [@class="input-group"] + [ + ./input + [@id="my&id"] + [@type="text"] + [@name="name"] + [@class="my&class form-control"] + [@value="10"] + /following-sibling::span + [@class="input-group-text"] + [contains(.., "‱")] + ] +' + ); + } + + public function testRange() + { + $form = $this->factory->createNamed('name', RangeType::class, 42, ['attr' => ['min' => 5]]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/input + [@type="range"] + [@name="name"] + [@value="42"] + [@min="5"] + [@class="my&class form-range"] +' + ); + } + + public function testRangeWithMinMaxValues() + { + $form = $this->factory->createNamed('name', RangeType::class, 42, ['attr' => ['min' => 5, 'max' => 57]]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/input + [@type="range"] + [@name="name"] + [@value="42"] + [@min="5"] + [@max="57"] + [@class="my&class form-range"] +' + ); + } + + public function testColor() + { + $color = '#0000ff'; + $form = $this->factory->createNamed('name', ColorType::class, $color); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/input + [@type="color"] + [@name="name"] + [@class="my&class form-control form-control-color"] + [@value="#0000ff"] +' + ); + } + + public function testTime() + { + $form = $this->factory->createNamed('name', TimeType::class, '04:05:06', [ + 'input' => 'string', + 'with_seconds' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_hour"] + [@class="form-select"] + [not(@size)] + [./option[@value="4"][@selected="selected"]] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_minute"] + [@class="form-select"] + [not(@size)] + [./option[@value="5"][@selected="selected"]] + ] + [count(./select)=2] + ] +' + ); + } + + public function testTimeWithSeconds() + { + $form = $this->factory->createNamed('name', TimeType::class, '04:05:06', [ + 'input' => 'string', + 'with_seconds' => true, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_hour"] + [@class="form-select"] + [not(@size)] + [./option[@value="4"][@selected="selected"]] + [count(./option)>23] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_minute"] + [@class="form-select"] + [not(@size)] + [./option[@value="5"][@selected="selected"]] + [count(./option)>59] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_second"] + [@class="form-select"] + [not(@size)] + [./option[@value="6"][@selected="selected"]] + [count(./option)>59] + ] + [count(./select)=3] + ] +' + ); + } + + public function testTimeText() + { + $form = $this->factory->createNamed('name', TimeType::class, '04:05:06', [ + 'input' => 'string', + 'widget' => 'text', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./input + [@type="text"] + [@id="name_hour"] + [@name="name[hour]"] + [@class="form-control"] + [@value="04"] + [@required="required"] + [not(@size)] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::input + [@type="text"] + [@id="name_minute"] + [@name="name[minute]"] + [@class="form-control"] + [@value="05"] + [@required="required"] + [not(@size)] + ] + [count(./input)=2] + ] +' + ); + } + + public function testTimeWithPlaceholderGlobal() + { + $form = $this->factory->createNamed('name', TimeType::class, null, [ + 'input' => 'string', + 'placeholder' => 'Change&Me', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_hour"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + [count(./option)>24] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_minute"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + [count(./option)>60] + ] + [count(./select)=2] + ] +' + ); + } + + public function testTimeWithPlaceholderOnYear() + { + $form = $this->factory->createNamed('name', TimeType::class, null, [ + 'input' => 'string', + 'required' => false, + 'placeholder' => ['hour' => 'Change&Me'], + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./div + [@class="input-group"] + [ + ./select + [@id="name_hour"] + [@class="form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]] + [count(./option)>24] + /following-sibling::span + [@class="input-group-text"] + /following-sibling::select + [@id="name_minute"] + [./option[@value="1"]] + [count(./option)>59] + ] + [count(./select)=2] + ] +' + ); + } + + public function testTimezone() + { + $form = $this->factory->createNamed('name', TimezoneType::class, 'Europe/Vienna'); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-select"] + [not(@required)] + [./option[@value="Europe/Vienna"][@selected="selected"][.="Europe / Vienna"]] + [count(.//option)>200] +' + ); + } + + public function testTimezoneWithPlaceholder() + { + $form = $this->factory->createNamed('name', TimezoneType::class, null, [ + 'placeholder' => 'Select&Timezone', + 'required' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/select + [@class="my&class form-select"] + [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Timezone[/trans]"]] + [count(.//option)>201] +' + ); + } + + public function testWeekChoices() + { + $this->requiresFeatureSet(404); + + $data = ['year' => (int) date('Y'), 'week' => 1]; + + $form = $this->factory->createNamed('name', WeekType::class, $data, [ + 'input' => 'array', + 'widget' => 'choice', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./select + [@id="name_year"] + [@class="form-select"] + [./option[@value="'.$data['year'].'"][@selected="selected"]] + /following-sibling::select + [@id="name_week"] + [@class="form-select"] + [./option[@value="'.$data['week'].'"][@selected="selected"]] + ] + [count(.//select)=2]' + ); + } + + public function testFloatingLabel() + { + $form = $this->factory->createNamed('name', TextType::class, null, [ + 'attr' => [ + 'placeholder' => 'name', + ], + 'row_attr' => [ + 'class' => 'form-floating mb-3', + ], + ]); + + $html = $this->renderRow($form->createView()); + + $this->assertMatchesXpath($html, + '/div + [@class="form-floating mb-3"] + [ + ./input + [@id="name"] + [@placeholder="[trans]name[/trans]"] + /following-sibling::label + [@for="name"] + ] +' + ); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php index 874faeeb99955..33bb266cb6a2f 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php @@ -44,7 +44,7 @@ public function testGettingMethodAbbreviation($method, $abbr) $this->assertEquals($this->getExtension()->abbrMethod($method), $abbr); } - public function getClassNameProvider() + public function getClassNameProvider(): array { return [ ['F\Q\N\Foo', 'Foo'], @@ -52,7 +52,7 @@ public function getClassNameProvider() ]; } - public function getMethodNameProvider() + public function getMethodNameProvider(): array { return [ ['F\Q\N\Foo::Method', 'Foo::Method()'], @@ -62,12 +62,7 @@ public function getMethodNameProvider() ]; } - public function testGetName() - { - $this->assertEquals('code', $this->getExtension()->getName()); - } - - protected function getExtension() + protected function getExtension(): CodeExtension { return new CodeExtension(new FileLinkFormatter('proto://%f#&line=%l&'.substr(__FILE__, 0, 5).'>foobar'), \DIRECTORY_SEPARATOR.'project', 'UTF-8'); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/SerializerModelFixture.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/SerializerModelFixture.php new file mode 100644 index 0000000000000..07493ea9d8db7 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/SerializerModelFixture.php @@ -0,0 +1,18 @@ + + */ +class SerializerModelFixture +{ + /** + * @Groups({"read"}) + */ + public $name = 'howdy'; + + public $title = 'fixture'; +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php deleted file mode 100644 index 8ee7830974861..0000000000000 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Twig\Tests\Extension\Fixtures; - -use Twig\Loader\FilesystemLoader; - -class StubFilesystemLoader extends FilesystemLoader -{ - /** - * @return string|null - */ - protected function findTemplate($name, $throw = true) - { - // strip away bundle name - $parts = explode(':', $name); - - return parent::findTemplate(end($parts), $throw); - } -} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php index 2c8c7db10d861..4c6e672a9af2d 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php @@ -19,4 +19,9 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul { return '[trans]'.strtr($id, $parameters).'[/trans]'; } + + public function getLocale(): string + { + return 'en'; + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php index 9f0b0c070a2d9..a0953a2d9f346 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php @@ -14,12 +14,12 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; -use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +use Twig\Loader\FilesystemLoader; class FormExtensionBootstrap3HorizontalLayoutTest extends AbstractBootstrap3HorizontalLayoutTest { @@ -38,7 +38,7 @@ protected function setUp(): void { parent::setUp(); - $loader = new StubFilesystemLoader([ + $loader = new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php index 38c445f927726..b02d859f74856 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php @@ -14,12 +14,12 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; -use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +use Twig\Loader\FilesystemLoader; class FormExtensionBootstrap3LayoutTest extends AbstractBootstrap3LayoutTest { @@ -34,7 +34,7 @@ protected function setUp(): void { parent::setUp(); - $loader = new StubFilesystemLoader([ + $loader = new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]); @@ -77,7 +77,7 @@ public function testStartTagHasActionAttributeWhenActionIsZero() public function testMoneyWidgetInIso() { - $environment = new Environment(new StubFilesystemLoader([ + $environment = new Environment(new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]), ['strict_variables' => true]); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php index 5c2e5afcfdf99..33e1862afd74a 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php @@ -14,12 +14,12 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; -use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +use Twig\Loader\FilesystemLoader; /** * Class providing test cases for the Bootstrap 4 Twig form theme. @@ -40,7 +40,7 @@ protected function setUp(): void { parent::setUp(); - $loader = new StubFilesystemLoader([ + $loader = new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php index 7dda79420ca12..41985a8ce2d85 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php @@ -14,12 +14,12 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; -use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +use Twig\Loader\FilesystemLoader; /** * Class providing test cases for the Bootstrap 4 horizontal Twig form theme. @@ -38,7 +38,7 @@ protected function setUp(): void { parent::setUp(); - $loader = new StubFilesystemLoader([ + $loader = new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]); @@ -81,7 +81,7 @@ public function testStartTagHasActionAttributeWhenActionIsZero() public function testMoneyWidgetInIso() { - $environment = new Environment(new StubFilesystemLoader([ + $environment = new Environment(new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]), ['strict_variables' => true]); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5HorizontalLayoutTest.php new file mode 100644 index 0000000000000..ef924884a4751 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5HorizontalLayoutTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\FormView; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Twig\Environment; +use Twig\Loader\FilesystemLoader; + +/** + * Class providing test cases for the Bootstrap 5 horizontal Twig form theme. + * + * @author Romain Monteil + */ +class FormExtensionBootstrap5HorizontalLayoutTest extends AbstractBootstrap5HorizontalLayoutTest +{ + use RuntimeLoaderProvider; + + protected $testableFeatures = [ + 'choice_attr', + ]; + + private $renderer; + + protected function setUp(): void + { + parent::setUp(); + + $loader = new FilesystemLoader([ + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + ]); + + $environment = new Environment($loader, ['strict_variables' => true]); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension(new FormExtension()); + + $rendererEngine = new TwigRendererEngine([ + 'bootstrap_5_horizontal_layout.html.twig', + 'custom_widgets.html.twig', + ], $environment); + $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock()); + $this->registerTwigRuntimeLoader($environment, $this->renderer); + } + + protected function renderForm(FormView $view, array $vars = []): string + { + return $this->renderer->renderBlock($view, 'form', $vars); + } + + protected function renderLabel(FormView $view, $label = null, array $vars = []): string + { + if (null !== $label) { + $vars += ['label' => $label]; + } + + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); + } + + protected function renderHelp(FormView $view): string + { + return $this->renderer->searchAndRenderBlock($view, 'help'); + } + + protected function renderErrors(FormView $view): string + { + return $this->renderer->searchAndRenderBlock($view, 'errors'); + } + + protected function renderWidget(FormView $view, array $vars = []): string + { + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + } + + protected function renderRow(FormView $view, array $vars = []): string + { + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); + } + + protected function renderRest(FormView $view, array $vars = []): string + { + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + } + + protected function renderStart(FormView $view, array $vars = []): string + { + return $this->renderer->renderBlock($view, 'form_start', $vars); + } + + protected function renderEnd(FormView $view, array $vars = []): string + { + return $this->renderer->renderBlock($view, 'form_end', $vars); + } + + protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true): void + { + $this->renderer->setTheme($view, $themes, $useDefaultThemes); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php new file mode 100644 index 0000000000000..8c0e54744f964 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap5LayoutTest.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Extension\TranslationExtension; +use Symfony\Bridge\Twig\Form\TwigRendererEngine; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\MoneyType; +use Symfony\Component\Form\FormRenderer; +use Symfony\Component\Form\FormView; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Twig\Environment; +use Twig\Loader\FilesystemLoader; + +/** + * Class providing test cases for the Bootstrap 5 Twig form theme. + * + * @author Romain Monteil + */ +class FormExtensionBootstrap5LayoutTest extends AbstractBootstrap5LayoutTest +{ + use RuntimeLoaderProvider; + + /** + * @var FormRenderer + */ + private $renderer; + + protected function setUp(): void + { + parent::setUp(); + + $loader = new FilesystemLoader([ + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + ]); + + $environment = new Environment($loader, ['strict_variables' => true]); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension(new FormExtension()); + + $rendererEngine = new TwigRendererEngine([ + 'bootstrap_5_layout.html.twig', + 'custom_widgets.html.twig', + ], $environment); + $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock()); + $this->registerTwigRuntimeLoader($environment, $this->renderer); + } + + public function testStartTagHasNoActionAttributeWhenActionIsEmpty() + { + $form = $this->factory->create(FormType::class, null, [ + 'method' => 'get', + 'action' => '', + ]); + + $html = $this->renderStart($form->createView()); + + self::assertSame('
', $html); + } + + public function testStartTagHasActionAttributeWhenActionIsZero() + { + $form = $this->factory->create(FormType::class, null, [ + 'method' => 'get', + 'action' => '0', + ]); + + $html = $this->renderStart($form->createView()); + + self::assertSame('', $html); + } + + public function testMoneyWidgetInIso() + { + $environment = new Environment(new FilesystemLoader([ + __DIR__.'/../../Resources/views/Form', + __DIR__.'/Fixtures/templates/form', + ]), ['strict_variables' => true]); + $environment->addExtension(new TranslationExtension(new StubTranslator())); + $environment->addExtension(new FormExtension()); + $environment->setCharset('ISO-8859-1'); + + $rendererEngine = new TwigRendererEngine([ + 'bootstrap_5_layout.html.twig', + 'custom_widgets.html.twig', + ], $environment); + $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock()); + $this->registerTwigRuntimeLoader($environment, $this->renderer); + + $view = $this->factory + ->createNamed('name', MoneyType::class) + ->createView(); + + self::assertSame(<<<'HTML' +
+HTML + , trim($this->renderWidget($view))); + } + + protected function renderForm(FormView $view, array $vars = []): string + { + return $this->renderer->renderBlock($view, 'form', $vars); + } + + protected function renderLabel(FormView $view, $label = null, array $vars = []): string + { + if (null !== $label) { + $vars += ['label' => $label]; + } + + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); + } + + protected function renderHelp(FormView $view): string + { + return $this->renderer->searchAndRenderBlock($view, 'help'); + } + + protected function renderErrors(FormView $view): string + { + return $this->renderer->searchAndRenderBlock($view, 'errors'); + } + + protected function renderWidget(FormView $view, array $vars = []): string + { + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + } + + protected function renderRow(FormView $view, array $vars = []): string + { + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); + } + + protected function renderRest(FormView $view, array $vars = []): string + { + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + } + + protected function renderStart(FormView $view, array $vars = []): string + { + return $this->renderer->renderBlock($view, 'form_start', $vars); + } + + protected function renderEnd(FormView $view, array $vars = []): string + { + return $this->renderer->renderBlock($view, 'form_end', $vars); + } + + protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true): void + { + $this->renderer->setTheme($view, $themes, $useDefaultThemes); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index 99477a617229c..e7991240f03f5 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -14,7 +14,6 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; -use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormRenderer; @@ -22,6 +21,7 @@ use Symfony\Component\Form\Tests\AbstractDivLayoutTest; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +use Twig\Loader\FilesystemLoader; class FormExtensionDivLayoutTest extends AbstractDivLayoutTest { @@ -36,7 +36,7 @@ protected function setUp(): void { parent::setUp(); - $loader = new StubFilesystemLoader([ + $loader = new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]); @@ -168,7 +168,7 @@ public function testIsRootForm($expected, FormView $formView) public function testMoneyWidgetInIso() { - $environment = new Environment(new StubFilesystemLoader([ + $environment = new Environment(new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]), ['strict_variables' => true]); @@ -294,6 +294,39 @@ public function testHelpHtmlIsTrue() ); } + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"]/b[.="Bolded label"]'); + } + protected function renderForm(FormView $view, array $vars = []) { return $this->renderer->renderBlock($view, 'form', $vars); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php new file mode 100644 index 0000000000000..ce3ee926e11b8 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionFieldHelpersTest.php @@ -0,0 +1,304 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Symfony\Bridge\Twig\Extension\FormExtension; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Test\FormIntegrationTestCase; + +class FormExtensionFieldHelpersTest extends FormIntegrationTestCase +{ + /** + * @var FormExtension + */ + private $rawExtension; + + /** + * @var FormExtension + */ + private $translatorExtension; + + /** + * @var FormView + */ + private $view; + + protected function getTypes() + { + return [new TextType(), new ChoiceType()]; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->rawExtension = new FormExtension(); + $this->translatorExtension = new FormExtension(new StubTranslator()); + + $data = [ + 'username' => 'tgalopin', + 'choice_multiple' => ['sugar', 'salt'], + ]; + + $form = $this->factory->createNamedBuilder('register', FormType::class, $data) + ->add('username', TextType::class, [ + 'label' => 'base.username', + 'label_translation_parameters' => ['%label_brand%' => 'Symfony'], + 'help' => 'base.username_help', + 'help_translation_parameters' => ['%help_brand%' => 'Symfony'], + 'translation_domain' => 'forms', + ]) + ->add('choice_flat', ChoiceType::class, [ + 'choices' => [ + 'base.yes' => 'yes', + 'base.no' => 'no', + ], + 'choice_translation_domain' => 'forms', + ]) + ->add('choice_grouped', ChoiceType::class, [ + 'choices' => [ + 'base.europe' => [ + 'base.fr' => 'fr', + 'base.de' => 'de', + ], + 'base.asia' => [ + 'base.cn' => 'cn', + 'base.jp' => 'jp', + ], + ], + 'choice_translation_domain' => 'forms', + 'label_format' => 'label format for field "%name%" with id "%id%"', + ]) + ->add('choice_multiple', ChoiceType::class, [ + 'choices' => [ + 'base.sugar' => 'sugar', + 'base.salt' => 'salt', + ], + 'multiple' => true, + 'expanded' => true, + 'label' => false, + ]) + ->getForm() + ; + + $form->get('username')->addError(new FormError('username.max_length')); + + $this->view = $form->createView(); + } + + public function testFieldName() + { + $this->assertFalse($this->view->children['username']->isRendered()); + $this->assertSame('register[username]', $this->rawExtension->getFieldName($this->view->children['username'])); + $this->assertTrue($this->view->children['username']->isRendered()); + } + + public function testFieldValue() + { + $this->assertSame('tgalopin', $this->rawExtension->getFieldValue($this->view->children['username'])); + $this->assertSame(['sugar', 'salt'], $this->rawExtension->getFieldValue($this->view->children['choice_multiple'])); + } + + public function testFieldLabel() + { + $this->assertSame('base.username', $this->rawExtension->getFieldLabel($this->view->children['username'])); + } + + public function testFieldTranslatedLabel() + { + $this->assertSame('[trans]base.username[/trans]', $this->translatorExtension->getFieldLabel($this->view->children['username'])); + } + + public function testFieldLabelFromFormat() + { + $this->assertSame('label format for field "choice_grouped" with id "register_choice_grouped"', $this->rawExtension->getFieldLabel($this->view->children['choice_grouped'])); + } + + public function testFieldLabelFallsBackToName() + { + $this->assertSame('Choice flat', $this->rawExtension->getFieldLabel($this->view->children['choice_flat'])); + } + + public function testFieldLabelReturnsNullWhenLabelIsDisabled() + { + $this->assertNull($this->rawExtension->getFieldLabel($this->view->children['choice_multiple'])); + } + + public function testFieldHelp() + { + $this->assertSame('base.username_help', $this->rawExtension->getFieldHelp($this->view->children['username'])); + } + + public function testFieldTranslatedHelp() + { + $this->assertSame('[trans]base.username_help[/trans]', $this->translatorExtension->getFieldHelp($this->view->children['username'])); + } + + public function testFieldErrors() + { + $errors = $this->rawExtension->getFieldErrors($this->view->children['username']); + $this->assertSame(['username.max_length'], iterator_to_array($errors)); + } + + public function testFieldTranslatedErrors() + { + $errors = $this->translatorExtension->getFieldErrors($this->view->children['username']); + $this->assertSame(['username.max_length'], iterator_to_array($errors)); + } + + public function testFieldChoicesFlat() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_flat']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('yes', $choicesArray[0]['value']); + $this->assertSame('base.yes', $choicesArray[0]['label']); + + $this->assertSame('no', $choicesArray[1]['value']); + $this->assertSame('base.no', $choicesArray[1]['label']); + } + + public function testFieldTranslatedChoicesFlat() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_flat']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('yes', $choicesArray[0]['value']); + $this->assertSame('[trans]base.yes[/trans]', $choicesArray[0]['label']); + + $this->assertSame('no', $choicesArray[1]['value']); + $this->assertSame('[trans]base.no[/trans]', $choicesArray[1]['label']); + } + + public function testFieldChoicesGrouped() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_grouped']); + + $choicesArray = []; + foreach ($choices as $groupLabel => $groupChoices) { + $groupChoicesArray = []; + foreach ($groupChoices as $label => $value) { + $groupChoicesArray[] = ['label' => $label, 'value' => $value]; + } + + $choicesArray[] = ['label' => $groupLabel, 'choices' => $groupChoicesArray]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertCount(2, $choicesArray[0]['choices']); + $this->assertSame('base.europe', $choicesArray[0]['label']); + + $this->assertSame('fr', $choicesArray[0]['choices'][0]['value']); + $this->assertSame('base.fr', $choicesArray[0]['choices'][0]['label']); + + $this->assertSame('de', $choicesArray[0]['choices'][1]['value']); + $this->assertSame('base.de', $choicesArray[0]['choices'][1]['label']); + + $this->assertCount(2, $choicesArray[1]['choices']); + $this->assertSame('base.asia', $choicesArray[1]['label']); + + $this->assertSame('cn', $choicesArray[1]['choices'][0]['value']); + $this->assertSame('base.cn', $choicesArray[1]['choices'][0]['label']); + + $this->assertSame('jp', $choicesArray[1]['choices'][1]['value']); + $this->assertSame('base.jp', $choicesArray[1]['choices'][1]['label']); + } + + public function testFieldTranslatedChoicesGrouped() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_grouped']); + + $choicesArray = []; + foreach ($choices as $groupLabel => $groupChoices) { + $groupChoicesArray = []; + foreach ($groupChoices as $label => $value) { + $groupChoicesArray[] = ['label' => $label, 'value' => $value]; + } + + $choicesArray[] = ['label' => $groupLabel, 'choices' => $groupChoicesArray]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertCount(2, $choicesArray[0]['choices']); + $this->assertSame('[trans]base.europe[/trans]', $choicesArray[0]['label']); + + $this->assertSame('fr', $choicesArray[0]['choices'][0]['value']); + $this->assertSame('[trans]base.fr[/trans]', $choicesArray[0]['choices'][0]['label']); + + $this->assertSame('de', $choicesArray[0]['choices'][1]['value']); + $this->assertSame('[trans]base.de[/trans]', $choicesArray[0]['choices'][1]['label']); + + $this->assertCount(2, $choicesArray[1]['choices']); + $this->assertSame('[trans]base.asia[/trans]', $choicesArray[1]['label']); + + $this->assertSame('cn', $choicesArray[1]['choices'][0]['value']); + $this->assertSame('[trans]base.cn[/trans]', $choicesArray[1]['choices'][0]['label']); + + $this->assertSame('jp', $choicesArray[1]['choices'][1]['value']); + $this->assertSame('[trans]base.jp[/trans]', $choicesArray[1]['choices'][1]['label']); + } + + public function testFieldChoicesMultiple() + { + $choices = $this->rawExtension->getFieldChoices($this->view->children['choice_multiple']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('sugar', $choicesArray[0]['value']); + $this->assertSame('base.sugar', $choicesArray[0]['label']); + + $this->assertSame('salt', $choicesArray[1]['value']); + $this->assertSame('base.salt', $choicesArray[1]['label']); + } + + public function testFieldTranslatedChoicesMultiple() + { + $choices = $this->translatorExtension->getFieldChoices($this->view->children['choice_multiple']); + + $choicesArray = []; + foreach ($choices as $label => $value) { + $choicesArray[] = ['label' => $label, 'value' => $value]; + } + + $this->assertCount(2, $choicesArray); + + $this->assertSame('sugar', $choicesArray[0]['value']); + $this->assertSame('[trans]base.sugar[/trans]', $choicesArray[0]['label']); + + $this->assertSame('salt', $choicesArray[1]['value']); + $this->assertSame('[trans]base.salt[/trans]', $choicesArray[1]['label']); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php index 967e25ec6ec45..20b465418daea 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php @@ -14,13 +14,13 @@ use Symfony\Bridge\Twig\Extension\FormExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Form\TwigRendererEngine; -use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader; use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Tests\AbstractTableLayoutTest; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; +use Twig\Loader\FilesystemLoader; class FormExtensionTableLayoutTest extends AbstractTableLayoutTest { @@ -35,7 +35,7 @@ protected function setUp(): void { parent::setUp(); - $loader = new StubFilesystemLoader([ + $loader = new FilesystemLoader([ __DIR__.'/../../Resources/views/Form', __DIR__.'/Fixtures/templates/form', ]); @@ -180,6 +180,39 @@ public function testHelpHtmlIsTrue() ); } + public function testLabelHtmlDefaultIsFalse() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"][.="[trans]Bolded label[/trans]"]'); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"]/b[.="Bolded label"]', 0); + } + + public function testLabelHtmlIsTrue() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ + 'label' => 'Bolded label', + 'label_html' => true, + ]); + + $html = $this->renderLabel($form->createView(), null, [ + 'label_attr' => [ + 'class' => 'my&class', + ], + ]); + + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"][.="[trans]Bolded label[/trans]"]', 0); + $this->assertMatchesXpath($html, '/label[@for="name"][@class="my&class required"]/b[.="Bolded label"]'); + } + protected function renderForm(FormView $view, array $vars = []) { return $this->renderer->renderBlock($view, 'form', $vars); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php index cb6658e4bbd32..ea3bb17bb3038 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php @@ -15,11 +15,9 @@ use Symfony\Bridge\Twig\Extension\HttpFoundationExtension; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\UrlHelper; use Symfony\Component\Routing\RequestContext; -/** - * @group legacy - */ class HttpFoundationExtensionTest extends TestCase { /** @@ -29,7 +27,7 @@ public function testGenerateAbsoluteUrl($expected, $path, $pathinfo) { $stack = new RequestStack(); $stack->push(Request::create($pathinfo)); - $extension = new HttpFoundationExtension($stack); + $extension = new HttpFoundationExtension(new UrlHelper($stack)); $this->assertEquals($expected, $extension->generateAbsoluteUrl($path)); } @@ -67,7 +65,7 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host } $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); - $extension = new HttpFoundationExtension(new RequestStack(), $requestContext); + $extension = new HttpFoundationExtension(new UrlHelper(new RequestStack(), $requestContext)); $this->assertEquals($expected, $extension->generateAbsoluteUrl($path)); } @@ -81,7 +79,7 @@ public function testGenerateAbsoluteUrlWithoutRequestAndRequestContext($path) $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); } - $extension = new HttpFoundationExtension(new RequestStack()); + $extension = new HttpFoundationExtension(new UrlHelper(new RequestStack())); $this->assertEquals($path, $extension->generateAbsoluteUrl($path)); } @@ -107,7 +105,7 @@ public function testGenerateAbsoluteUrlWithScriptFileName() $stack = new RequestStack(); $stack->push($request); - $extension = new HttpFoundationExtension($stack); + $extension = new HttpFoundationExtension(new UrlHelper($stack)); $this->assertEquals( 'http://localhost/app/web/bundles/framework/css/structure.css', @@ -126,7 +124,7 @@ public function testGenerateRelativePath($expected, $path, $pathinfo) $stack = new RequestStack(); $stack->push(Request::create($pathinfo)); - $extension = new HttpFoundationExtension($stack); + $extension = new HttpFoundationExtension(new UrlHelper($stack)); $this->assertEquals($expected, $extension->generateRelativePath($path)); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 5fa1ef3bad62c..a3294db0d2ae6 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -14,11 +14,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\HttpKernelExtension; use Symfony\Bridge\Twig\Extension\HttpKernelRuntime; +use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; +use Symfony\Component\HttpKernel\Fragment\FragmentUriGenerator; +use Symfony\Component\HttpKernel\UriSigner; use Twig\Environment; use Twig\Loader\ArrayLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; @@ -53,6 +56,37 @@ public function testUnknownFragmentRenderer() $renderer->render('/foo'); } + public function testGenerateFragmentUri() + { + if (!class_exists(FragmentUriGenerator::class)) { + $this->markTestSkipped('HttpKernel 5.3+ is required'); + } + + $requestStack = new RequestStack(); + $requestStack->push(Request::create('/')); + + $fragmentHandler = new FragmentHandler($requestStack); + $fragmentUriGenerator = new FragmentUriGenerator('/_fragment', new UriSigner('s3cr3t'), $requestStack); + + $kernelRuntime = new HttpKernelRuntime($fragmentHandler, $fragmentUriGenerator); + + $loader = new ArrayLoader([ + 'index' => sprintf(<< true, 'cache' => false]); + $twig->addExtension(new HttpKernelExtension()); + + $loader = $this->createMock(RuntimeLoaderInterface::class); + $loader->expects($this->any())->method('load')->willReturnMap([ + [HttpKernelRuntime::class, $kernelRuntime], + ]); + $twig->addRuntimeLoader($loader); + + $this->assertSame('/_fragment?_hash=PP8%2FeEbn1pr27I9wmag%2FM6jYGVwUZ0l2h0vhh2OJ6CI%3D&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfonyBundleFrameworkBundleControllerTemplateController%253A%253AtemplateAction', $twig->render('index')); + } + protected function getFragmentHandler($return) { $strategy = $this->createMock(FragmentRendererInterface::class); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php new file mode 100644 index 0000000000000..ef54ee2775f15 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Extension; + +use Doctrine\Common\Annotations\AnnotationReader; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Extension\SerializerExtension; +use Symfony\Bridge\Twig\Extension\SerializerRuntime; +use Symfony\Bridge\Twig\Tests\Extension\Fixtures\SerializerModelFixture; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\YamlEncoder; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; +use Twig\Environment; +use Twig\Loader\ArrayLoader; +use Twig\RuntimeLoader\RuntimeLoaderInterface; + +/** + * @author Jesse Rushlow + */ +class SerializerExtensionTest extends TestCase +{ + /** + * @dataProvider serializerDataProvider + */ + public function testSerializeFilter(string $template, string $expectedResult) + { + $twig = $this->getTwig($template); + + self::assertSame($expectedResult, $twig->render('template', ['object' => new SerializerModelFixture()])); + } + + public function serializerDataProvider(): \Generator + { + yield ['{{ object|serialize }}', '{"name":"howdy","title":"fixture"}']; + yield ['{{ object|serialize(\'yaml\') }}', '{ name: howdy, title: fixture }']; + yield ['{{ object|serialize(\'yaml\', {groups: \'read\'}) }}', '{ name: howdy }']; + } + + private function getTwig(string $template): Environment + { + $meta = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $runtime = new SerializerRuntime(new Serializer([new ObjectNormalizer($meta)], [new JsonEncoder(), new YamlEncoder()])); + + $mockRuntimeLoader = $this->createMock(RuntimeLoaderInterface::class); + $mockRuntimeLoader + ->method('load') + ->willReturnMap([ + ['Symfony\Bridge\Twig\Extension\SerializerRuntime', $runtime], + ]) + ; + + $twig = new Environment(new ArrayLoader(['template' => $template])); + $twig->addExtension(new SerializerExtension()); + $twig->addRuntimeLoader($mockRuntimeLoader); + + return $twig; + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index a02fd2b29552d..86f50da0b7db8 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -47,15 +47,6 @@ public function testTrans($template, $expected, array $variables = []) $this->assertEquals($expected, $this->getTemplate($template)->render($variables)); } - /** - * @group legacy - * @dataProvider getTransChoiceTests - */ - public function testTransChoice($template, $expected, array $variables = []) - { - $this->testTrans($template, $expected, $variables); - } - public function testTransUnknownKeyword() { $this->expectException(\Twig\Error\SyntaxError::class); @@ -70,16 +61,6 @@ public function testTransComplexBody() $this->getTemplate("{% trans %}\n{{ 1 + 2 }}{% endtrans %}")->render(); } - /** - * @group legacy - */ - public function testTransChoiceComplexBody() - { - $this->expectException(\Twig\Error\SyntaxError::class); - $this->expectExceptionMessage('A message inside a transchoice tag must be a simple text in "index" at line 2.'); - $this->getTemplate("{% transchoice count %}\n{{ 1 + 2 }}{% endtranschoice %}")->render(); - } - public function getTransTests() { return [ @@ -137,69 +118,22 @@ public function getTransTests() ['{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|trans(count=count) }}', 'There is 5 apples', ['count' => 5]], ['{{ text|trans(count=5, arguments={\'%name%\': \'Symfony\'}) }}', 'There is 5 apples (Symfony)', ['text' => '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%)']], ['{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|trans({}, "messages", "fr", count) }}', 'There is 5 apples', ['count' => 5]], - ]; - } - - /** - * @group legacy - */ - public function getTransChoiceTests() - { - return [ - // trans tag - ['{% trans %}Hello{% endtrans %}', 'Hello'], - ['{% trans %}%name%{% endtrans %}', 'Symfony', ['name' => 'Symfony']], - - ['{% trans from elsewhere %}Hello{% endtrans %}', 'Hello'], - - ['{% trans %}Hello %name%{% endtrans %}', 'Hello Symfony', ['name' => 'Symfony']], - ['{% trans with { \'%name%\': \'Symfony\' } %}Hello %name%{% endtrans %}', 'Hello Symfony'], - ['{% set vars = { \'%name%\': \'Symfony\' } %}{% trans with vars %}Hello %name%{% endtrans %}', 'Hello Symfony'], - - ['{% trans into "fr"%}Hello{% endtrans %}', 'Hello'], - - // transchoice - [ - '{% transchoice count from "messages" %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}', - 'There is no apples', - ['count' => 0], - ], - [ - '{% transchoice count %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}', - 'There is 5 apples', - ['count' => 5], - ], - [ - '{% transchoice count %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%){% endtranschoice %}', - 'There is 5 apples (Symfony)', - ['count' => 5, 'name' => 'Symfony'], - ], - [ - '{% transchoice count with { \'%name%\': \'Symfony\' } %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%){% endtranschoice %}', - 'There is 5 apples (Symfony)', - ['count' => 5], - ], - [ - '{% transchoice count into "fr"%}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}', - 'There is no apples', - ['count' => 0], - ], - [ - '{% transchoice 5 into "fr"%}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}', - 'There is 5 apples', - ], - - // trans filter - ['{{ "Hello"|trans }}', 'Hello'], - ['{{ name|trans }}', 'Symfony', ['name' => 'Symfony']], - ['{{ hello|trans({ \'%name%\': \'Symfony\' }) }}', 'Hello Symfony', ['hello' => 'Hello %name%']], - ['{% set vars = { \'%name%\': \'Symfony\' } %}{{ hello|trans(vars) }}', 'Hello Symfony', ['hello' => 'Hello %name%']], - ['{{ "Hello"|trans({}, "messages", "fr") }}', 'Hello'], - // transchoice filter - ['{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|transchoice(count) }}', 'There is 5 apples', ['count' => 5]], - ['{{ text|transchoice(5, {\'%name%\': \'Symfony\'}) }}', 'There is 5 apples (Symfony)', ['text' => '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%)']], - ['{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|transchoice(count, {}, "messages", "fr") }}', 'There is 5 apples', ['count' => 5]], + // trans filter with null message + ['{{ null|trans }}', ''], + ['{{ foo|trans }}', '', ['foo' => null]], + + // trans object + ['{{ t("Hello")|trans }}', 'Hello'], + ['{{ t(name)|trans }}', 'Symfony', ['name' => 'Symfony']], + ['{{ t(hello, { \'%name%\': \'Symfony\' })|trans }}', 'Hello Symfony', ['hello' => 'Hello %name%']], + ['{% set vars = { \'%name%\': \'Symfony\' } %}{{ t(hello, vars)|trans }}', 'Hello Symfony', ['hello' => 'Hello %name%']], + ['{{ t("Hello")|trans("fr") }}', 'Hello'], + ['{{ t("Hello")|trans(locale="fr") }}', 'Hello'], + ['{{ t("Hello", {}, "messages")|trans(locale="fr") }}', 'Hello'], + + // trans object with count + ['{{ t("{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples", {\'%count%\': count})|trans }}', 'There is 5 apples', ['count' => 5]], ]; } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php index 57a09b0a7e918..23dcc64b3d418 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php @@ -81,6 +81,16 @@ public function testGetEnabledTransitions() $this->assertSame('t1', $transitions[0]->getName()); } + public function testGetEnabledTransition() + { + $subject = new Subject(); + + $transition = $this->extension->getEnabledTransition($subject, 't1'); + + $this->assertInstanceOf(Transition::class, $transition); + $this->assertSame('t1', $transition->getName()); + } + public function testHasMarkedPlace() { $subject = new Subject(['ordered' => 1, 'waiting_for_payment' => 1]); diff --git a/src/Symfony/Bridge/Twig/Tests/Fixtures/TokenInterface.php b/src/Symfony/Bridge/Twig/Tests/Fixtures/TokenInterface.php deleted file mode 100644 index b22e99e66d22b..0000000000000 --- a/src/Symfony/Bridge/Twig/Tests/Fixtures/TokenInterface.php +++ /dev/null @@ -1,11 +0,0 @@ - true, 'raw' => false, 'a' => 'b', + 'footer_text' => 'Notification e-mail sent by Symfony', ], $email->getContext()); } @@ -47,6 +48,7 @@ public function testSerialize() 'markdown' => false, 'raw' => true, 'a' => 'b', + 'footer_text' => 'Notification e-mail sent by Symfony', ], $email->getContext()); } @@ -63,4 +65,55 @@ public function testSubject() $headers = $email->getPreparedHeaders(); $this->assertSame('[LOW] Foo', $headers->get('Subject')->getValue()); } + + public function testPublicMail() + { + $email = NotificationEmail::asPublicEmail() + ->markdown('Foo') + ->action('Bar', 'http://example.com/') + ->context(['a' => 'b']) + ; + + $this->assertEquals([ + 'importance' => null, + 'content' => 'Foo', + 'exception' => false, + 'action_text' => 'Bar', + 'action_url' => 'http://example.com/', + 'markdown' => true, + 'raw' => false, + 'a' => 'b', + 'footer_text' => null, + ], $email->getContext()); + + $email = (new NotificationEmail()) + ->markAsPublic() + ->markdown('Foo') + ->action('Bar', 'http://example.com/') + ->context(['a' => 'b']) + ; + + $this->assertEquals([ + 'importance' => null, + 'content' => 'Foo', + 'exception' => false, + 'action_text' => 'Bar', + 'action_url' => 'http://example.com/', + 'markdown' => true, + 'raw' => false, + 'a' => 'b', + 'footer_text' => null, + ], $email->getContext()); + } + + public function testPublicMailSubject() + { + $email = NotificationEmail::asPublicEmail()->from('me@example.com')->subject('Foo'); + $headers = $email->getPreparedHeaders(); + $this->assertSame('Foo', $headers->get('Subject')->getValue()); + + $email = (new NotificationEmail())->markAsPublic()->from('me@example.com')->subject('Foo'); + $headers = $email->getPreparedHeaders(); + $this->assertSame('Foo', $headers->get('Subject')->getValue()); + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php index 999ca4d078d58..2da04921446eb 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php @@ -4,6 +4,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Serializer; class TemplatedEmailTest extends TestCase { @@ -33,4 +40,77 @@ public function testSerialize() $this->assertEquals('text.html.twig', $email->getHtmlTemplate()); $this->assertEquals($context, $email->getContext()); } + + public function testSymfonySerialize() + { + // we don't add from/sender to check that validation is not triggered to serialize an email + $e = new TemplatedEmail(); + $e->to('you@example.com'); + $e->textTemplate('email.txt.twig'); + $e->htmlTemplate('email.html.twig'); + $e->context(['foo' => 'bar']); + $e->attach('Some Text file', 'test.txt'); + $expected = clone $e; + + $expectedJson = <<serialize($e, 'json'); + $this->assertSame($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + + $n = $serializer->deserialize($serialized, TemplatedEmail::class, 'json'); + $serialized = $serializer->serialize($e, 'json'); + $this->assertSame($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + + $n->from('fabien@symfony.com'); + $expected->from('fabien@symfony.com'); + $this->assertEquals($expected->getHeaders(), $n->getHeaders()); + $this->assertEquals($expected->getBody(), $n->getBody()); + } } diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php index 1c3d42d1a3192..cc2b6ef2ac39e 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php @@ -80,15 +80,11 @@ public function getDefaultDomainAssignmentTestData() { return [ [TwigNodeProvider::getTransFilter(self::$message)], - [TwigNodeProvider::getTransChoiceFilter(self::$message)], [TwigNodeProvider::getTransTag(self::$message)], // with named arguments [TwigNodeProvider::getTransFilter(self::$message, null, [ 'arguments' => new ArrayExpression([], 0), ])], - [TwigNodeProvider::getTransChoiceFilter(self::$message), null, [ - 'arguments' => new ArrayExpression([], 0), - ]], ]; } } diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php index 8d8b77c3acf53..069914a4fc066 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php @@ -58,10 +58,8 @@ public function getMessagesExtractionTestData() return [ [TwigNodeProvider::getTransFilter($message), [[$message, null]]], - [TwigNodeProvider::getTransChoiceFilter($message), [[$message, null]]], [TwigNodeProvider::getTransTag($message), [[$message, null]]], [TwigNodeProvider::getTransFilter($message, $domain), [[$message, $domain]]], - [TwigNodeProvider::getTransChoiceFilter($message, $domain), [[$message, $domain]]], [TwigNodeProvider::getTransTag($message, $domain), [[$message, $domain]]], ]; } diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php index 5147724675817..69311afdc824d 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TwigNodeProvider.php @@ -53,24 +53,6 @@ public static function getTransFilter($message, $domain = null, $arguments = nul ); } - public static function getTransChoiceFilter($message, $domain = null, $arguments = null) - { - if (!$arguments) { - $arguments = $domain ? [ - new ConstantExpression(0, 0), - new ArrayExpression([], 0), - new ConstantExpression($domain, 0), - ] : []; - } - - return new FilterExpression( - new ConstantExpression($message, 0), - new ConstantExpression('transchoice', 0), - new Node($arguments), - 0 - ); - } - public static function getTransTag($message, $domain = null) { return new TransNode( diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index 5dfc6ea450e0e..4a8b4d19c066e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -44,21 +44,16 @@ public function testExtract($template, $messages) $m->setAccessible(true); $m->invoke($extractor, $template, $catalogue); + if (0 === \count($messages)) { + $this->assertSame($catalogue->all(), $messages); + } + foreach ($messages as $key => $domain) { $this->assertTrue($catalogue->has($key, $domain)); $this->assertEquals('prefix'.$key, $catalogue->get($key, $domain)); } } - /** - * @group legacy - * @dataProvider getLegacyExtractData - */ - public function testLegacyExtract($template, $messages) - { - $this->testExtract($template, $messages); - } - public function getExtractData() { return [ @@ -70,6 +65,10 @@ public function getExtractData() ['{% trans from "domain" %}new key{% endtrans %}', ['new key' => 'domain']], ['{% set foo = "new key" | trans %}', ['new key' => 'messages']], ['{{ 1 ? "new key" | trans : "another key" | trans }}', ['new key' => 'messages', 'another key' => 'messages']], + ['{{ t("new key") | trans() }}', ['new key' => 'messages']], + ['{% set foo = t("new key") %}', ['new key' => 'messages']], + ['{{ t("new key", {}, "domain") | trans() }}', ['new key' => 'domain']], + ['{{ 1 ? t("new key") | trans : t("another key") | trans }}', ['new key' => 'messages', 'another key' => 'messages']], // make sure 'trans_default_domain' tag is supported ['{% trans_default_domain "domain" %}{{ "new key"|trans }}', ['new key' => 'domain']], @@ -77,24 +76,15 @@ public function getExtractData() // make sure this works with twig's named arguments ['{{ "new key" | trans(domain="domain") }}', ['new key' => 'domain']], - ]; - } - /** - * @group legacy - */ - public function getLegacyExtractData() - { - return [ - ['{{ "new key" | transchoice(1) }}', ['new key' => 'messages']], - ['{{ "new key" | transchoice(1) | upper }}', ['new key' => 'messages']], - ['{{ "new key" | transchoice(1, {}, "domain") }}', ['new key' => 'domain']], - - // make sure 'trans_default_domain' tag is supported - ['{% trans_default_domain "domain" %}{{ "new key"|transchoice }}', ['new key' => 'domain']], - - // make sure this works with twig's named arguments - ['{{ "new key" | transchoice(domain="domain", count=1) }}', ['new key' => 'domain']], + // concat translations + ['{{ ("new" ~ " key") | trans() }}', ['new key' => 'messages']], + ['{{ ("another " ~ "new " ~ "key") | trans() }}', ['another new key' => 'messages']], + ['{{ ("new" ~ " key") | trans(domain="domain") }}', ['new key' => 'domain']], + ['{{ ("another " ~ "new " ~ "key") | trans(domain="domain") }}', ['another new key' => 'domain']], + // if it has a variable or other expression, we cannot extract it + ['{% set foo = "new" %} {{ ("new " ~ foo ~ "key") | trans() }}', []], + ['{{ ("foo " ~ "new"|trans ~ "key") | trans() }}', ['new' => 'messages']], ]; } diff --git a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php b/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php deleted file mode 100644 index 6f504cb399f36..0000000000000 --- a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Twig\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Bridge\Twig\TwigEngine; -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReference; -use Twig\Environment; -use Twig\Error\SyntaxError; -use Twig\Loader\ArrayLoader; -use Twig\Template; - -/** - * @group legacy - */ -class TwigEngineTest extends TestCase -{ - public function testExistsWithTemplateInstances() - { - $engine = $this->getTwig(); - - $this->assertTrue($engine->exists($this->getMockForAbstractClass(Template::class, [], '', false))); - } - - public function testExistsWithNonExistentTemplates() - { - $engine = $this->getTwig(); - - $this->assertFalse($engine->exists('foobar')); - $this->assertFalse($engine->exists(new TemplateReference('foorbar'))); - } - - public function testExistsWithTemplateWithSyntaxErrors() - { - $engine = $this->getTwig(); - - $this->assertTrue($engine->exists('error')); - $this->assertTrue($engine->exists(new TemplateReference('error'))); - } - - public function testExists() - { - $engine = $this->getTwig(); - - $this->assertTrue($engine->exists('index')); - $this->assertTrue($engine->exists(new TemplateReference('index'))); - } - - public function testRender() - { - $engine = $this->getTwig(); - - $this->assertSame('foo', $engine->render('index')); - $this->assertSame('foo', $engine->render(new TemplateReference('index'))); - } - - public function testRenderWithError() - { - $this->expectException(SyntaxError::class); - $engine = $this->getTwig(); - - $engine->render(new TemplateReference('error')); - } - - protected function getTwig() - { - $twig = new Environment(new ArrayLoader([ - 'index' => 'foo', - 'error' => '{{ foo }', - ])); - $parser = $this->createMock(TemplateNameParserInterface::class); - - return new TwigEngine($twig, $parser); - } -} diff --git a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php index c13297f23258d..341dc41855ab0 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php @@ -26,17 +26,13 @@ * {% dump foo, bar %} * * @author Julien Galenski - * - * @final since Symfony 4.4 */ -class DumpTokenParser extends AbstractTokenParser +final class DumpTokenParser extends AbstractTokenParser { /** * {@inheritdoc} - * - * @return Node */ - public function parse(Token $token) + public function parse(Token $token): Node { $values = null; if (!$this->parser->getStream()->test(Token::BLOCK_END_TYPE)) { @@ -49,10 +45,8 @@ public function parse(Token $token) /** * {@inheritdoc} - * - * @return string */ - public function getTag() + public function getTag(): string { return 'dump'; } diff --git a/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php index 58fe3dd6be261..ef5dacb59ddd1 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php @@ -21,17 +21,13 @@ * Token Parser for the 'form_theme' tag. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class FormThemeTokenParser extends AbstractTokenParser +final class FormThemeTokenParser extends AbstractTokenParser { /** - * Parses a token and returns a node. - * - * @return Node + * {@inheritdoc} */ - public function parse(Token $token) + public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); @@ -59,11 +55,9 @@ public function parse(Token $token) } /** - * Gets the tag name associated with this token parser. - * - * @return string The tag name + * {@inheritdoc} */ - public function getTag() + public function getTag(): string { return 'form_theme'; } diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php index e1ac39cf8f390..f0b3ac2e9240e 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php @@ -21,22 +21,17 @@ * Token Parser for the stopwatch tag. * * @author Wouter J - * - * @final since Symfony 4.4 */ -class StopwatchTokenParser extends AbstractTokenParser +final class StopwatchTokenParser extends AbstractTokenParser { - protected $stopwatchIsAvailable; + private bool $stopwatchIsAvailable; public function __construct(bool $stopwatchIsAvailable) { $this->stopwatchIsAvailable = $stopwatchIsAvailable; } - /** - * @return Node - */ - public function parse(Token $token) + public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); @@ -57,15 +52,12 @@ public function parse(Token $token) return $body; } - public function decideStopwatchEnd(Token $token) + public function decideStopwatchEnd(Token $token): bool { return $token->test('endstopwatch'); } - /** - * @return string - */ - public function getTag() + public function getTag(): string { return 'stopwatch'; } diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php deleted file mode 100644 index c218091c70ff3..0000000000000 --- a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Twig\TokenParser; - -use Symfony\Bridge\Twig\Node\TransNode; -use Twig\Error\SyntaxError; -use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Node; -use Twig\Node\TextNode; -use Twig\Token; -use Twig\TokenParser\AbstractTokenParser; - -/** - * Token Parser for the 'transchoice' tag. - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use the "trans" tag with a "%count%" parameter instead - * - * @final since Symfony 4.4 - */ -class TransChoiceTokenParser extends AbstractTokenParser -{ - /** - * {@inheritdoc} - * - * @return Node - */ - public function parse(Token $token) - { - $lineno = $token->getLine(); - $stream = $this->parser->getStream(); - - @trigger_error(sprintf('The "transchoice" tag is deprecated since Symfony 4.2, use the "trans" one instead with a "%%count%%" parameter in %s line %d.', $stream->getSourceContext()->getName(), $lineno), \E_USER_DEPRECATED); - - $vars = new ArrayExpression([], $lineno); - - $count = $this->parser->getExpressionParser()->parseExpression(); - - $domain = null; - $locale = null; - - if ($stream->test('with')) { - // {% transchoice count with vars %} - $stream->next(); - $vars = $this->parser->getExpressionParser()->parseExpression(); - } - - if ($stream->test('from')) { - // {% transchoice count from "messages" %} - $stream->next(); - $domain = $this->parser->getExpressionParser()->parseExpression(); - } - - if ($stream->test('into')) { - // {% transchoice count into "fr" %} - $stream->next(); - $locale = $this->parser->getExpressionParser()->parseExpression(); - } - - $stream->expect(Token::BLOCK_END_TYPE); - - $body = $this->parser->subparse([$this, 'decideTransChoiceFork'], true); - - if (!$body instanceof TextNode && !$body instanceof AbstractExpression) { - throw new SyntaxError('A message inside a transchoice tag must be a simple text.', $body->getTemplateLine(), $stream->getSourceContext()); - } - - $stream->expect(Token::BLOCK_END_TYPE); - - return new TransNode($body, $domain, $count, $vars, $locale, $lineno, $this->getTag()); - } - - public function decideTransChoiceFork($token) - { - return $token->test(['endtranschoice']); - } - - /** - * {@inheritdoc} - * - * @return string - */ - public function getTag() - { - return 'transchoice'; - } -} diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php index e6b16680f7b7c..19b820497d9fa 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php @@ -20,17 +20,13 @@ * Token Parser for the 'trans_default_domain' tag. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class TransDefaultDomainTokenParser extends AbstractTokenParser +final class TransDefaultDomainTokenParser extends AbstractTokenParser { /** * {@inheritdoc} - * - * @return Node */ - public function parse(Token $token) + public function parse(Token $token): Node { $expr = $this->parser->getExpressionParser()->parseExpression(); @@ -41,10 +37,8 @@ public function parse(Token $token) /** * {@inheritdoc} - * - * @return string */ - public function getTag() + public function getTag(): string { return 'trans_default_domain'; } diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php index e72240b6e8119..ffe8828590852 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php @@ -24,17 +24,13 @@ * Token Parser for the 'trans' tag. * * @author Fabien Potencier - * - * @final since Symfony 4.4 */ -class TransTokenParser extends AbstractTokenParser +final class TransTokenParser extends AbstractTokenParser { /** * {@inheritdoc} - * - * @return Node */ - public function parse(Token $token) + public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); @@ -84,17 +80,15 @@ public function parse(Token $token) return new TransNode($body, $domain, $count, $vars, $locale, $lineno, $this->getTag()); } - public function decideTransFork($token) + public function decideTransFork(Token $token): bool { return $token->test(['endtrans']); } /** * {@inheritdoc} - * - * @return string */ - public function getTag() + public function getTag(): string { return 'trans'; } diff --git a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php index 3d01ca57c7158..1ec9a13d2d5cf 100644 --- a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php +++ b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php @@ -29,17 +29,13 @@ class TwigExtractor extends AbstractFileExtractor implements ExtractorInterface { /** * Default domain for found messages. - * - * @var string */ - private $defaultDomain = 'messages'; + private string $defaultDomain = 'messages'; /** * Prefix for found message. - * - * @var string */ - private $prefix = ''; + private string $prefix = ''; private $twig; @@ -65,12 +61,12 @@ public function extract($resource, MessageCatalogue $catalogue) /** * {@inheritdoc} */ - public function setPrefix($prefix) + public function setPrefix(string $prefix) { $this->prefix = $prefix; } - protected function extractTemplate($template, MessageCatalogue $catalogue) + protected function extractTemplate(string $template, MessageCatalogue $catalogue) { $visitor = $this->twig->getExtension('Symfony\Bridge\Twig\Extension\TranslationExtension')->getTranslationNodeVisitor(); $visitor->enable(); @@ -84,12 +80,7 @@ protected function extractTemplate($template, MessageCatalogue $catalogue) $visitor->disable(); } - /** - * @param string $file - * - * @return bool - */ - protected function canBeExtracted($file) + protected function canBeExtracted(string $file): bool { return $this->isFile($file) && 'twig' === pathinfo($file, \PATHINFO_EXTENSION); } @@ -97,7 +88,7 @@ protected function canBeExtracted($file) /** * {@inheritdoc} */ - protected function extractFromDirectory($directory) + protected function extractFromDirectory($directory): iterable { $finder = new Finder(); diff --git a/src/Symfony/Bridge/Twig/TwigEngine.php b/src/Symfony/Bridge/Twig/TwigEngine.php deleted file mode 100644 index c31f0a470557a..0000000000000 --- a/src/Symfony/Bridge/Twig/TwigEngine.php +++ /dev/null @@ -1,140 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Twig; - -@trigger_error('The '.TwigEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use \Twig\Environment instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Templating\EngineInterface; -use Symfony\Component\Templating\StreamingEngineInterface; -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReferenceInterface; -use Twig\Environment; -use Twig\Error\Error; -use Twig\Error\LoaderError; -use Twig\Loader\ExistsLoaderInterface; -use Twig\Loader\SourceContextLoaderInterface; -use Twig\Template; - -/** - * This engine knows how to render Twig templates. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TwigEngine implements EngineInterface, StreamingEngineInterface -{ - protected $environment; - protected $parser; - - public function __construct(Environment $environment, TemplateNameParserInterface $parser) - { - $this->environment = $environment; - $this->parser = $parser; - } - - /** - * {@inheritdoc} - * - * It also supports Template as name parameter. - * - * @throws Error if something went wrong like a thrown exception while rendering the template - */ - public function render($name, array $parameters = []) - { - return $this->load($name)->render($parameters); - } - - /** - * {@inheritdoc} - * - * It also supports Template as name parameter. - * - * @throws Error if something went wrong like a thrown exception while rendering the template - */ - public function stream($name, array $parameters = []) - { - $this->load($name)->display($parameters); - } - - /** - * {@inheritdoc} - * - * It also supports Template as name parameter. - */ - public function exists($name) - { - if ($name instanceof Template) { - return true; - } - - $loader = $this->environment->getLoader(); - - if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { - try { - // cast possible TemplateReferenceInterface to string because the - // EngineInterface supports them but LoaderInterface does not - if ($loader instanceof SourceContextLoaderInterface) { - $loader->getSourceContext((string) $name); - } else { - $loader->getSource((string) $name); - } - - return true; - } catch (LoaderError $e) { - } - - return false; - } - - return $loader->exists((string) $name); - } - - /** - * {@inheritdoc} - * - * It also supports Template as name parameter. - */ - public function supports($name) - { - if ($name instanceof Template) { - return true; - } - - $template = $this->parser->parse($name); - - return 'twig' === $template->get('engine'); - } - - /** - * Loads the given template. - * - * @param string|TemplateReferenceInterface|Template $name A template name or an instance of - * TemplateReferenceInterface or Template - * - * @return Template - * - * @throws \InvalidArgumentException if the template does not exist - */ - protected function load($name) - { - if ($name instanceof Template) { - return $name; - } - - try { - return $this->environment->load((string) $name)->unwrap(); - } catch (LoaderError $e) { - throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e); - } - } -} diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php index 16381fddac6c8..30dd92ff2ff3b 100644 --- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php +++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FullStack; use Twig\Error\SyntaxError; +use Twig\TwigFilter; +use Twig\TwigFunction; /** * @internal @@ -22,7 +24,6 @@ class UndefinedCallableHandler private const FILTER_COMPONENTS = [ 'humanize' => 'form', 'trans' => 'translation', - 'transchoice' => 'translation', 'yaml_encode' => 'yaml', 'yaml_dump' => 'yaml', ]; @@ -31,6 +32,8 @@ class UndefinedCallableHandler 'asset' => 'asset', 'asset_version' => 'asset', 'dump' => 'debug-bundle', + 'encore_entry_link_tags' => 'webpack-encore-bundle', + 'encore_entry_script_tags' => 'webpack-encore-bundle', 'expression' => 'expression-language', 'form_widget' => 'form', 'form_errors' => 'form', @@ -65,34 +68,34 @@ class UndefinedCallableHandler 'workflow' => 'enable "framework.workflows"', ]; - public static function onUndefinedFilter(string $name): bool + public static function onUndefinedFilter(string $name): TwigFilter|false { if (!isset(self::FILTER_COMPONENTS[$name])) { return false; } - self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name]); - - return true; + throw new SyntaxError(self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name])); } - public static function onUndefinedFunction(string $name): bool + public static function onUndefinedFunction(string $name): TwigFunction|false { if (!isset(self::FUNCTION_COMPONENTS[$name])) { return false; } - self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name]); + if ('webpack-encore-bundle' === self::FUNCTION_COMPONENTS[$name]) { + return new TwigFunction($name, static function () { return ''; }); + } - return true; + throw new SyntaxError(self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name])); } - private static function onUndefined(string $name, string $type, string $component) + private static function onUndefined(string $name, string $type, string $component): string { if (class_exists(FullStack::class) && isset(self::FULL_STACK_ENABLE[$component])) { - throw new SyntaxError(sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name)); + return sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name); } - throw new SyntaxError(sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name)); + return sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name); } } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 14c737127e13b..0765818346b6d 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -16,46 +16,50 @@ } ], "require": { - "php": ">=7.1.3", - "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^1.1|^2", - "twig/twig": "^1.43|^2.13|^3.0.4" + "php": ">=8.0.2", + "symfony/translation-contracts": "^1.1|^2|^3", + "twig/twig": "^2.13|^3.0.4" }, "require-dev": { + "doctrine/annotations": "^1.12", "egulias/email-validator": "^2.1.10|^3", - "symfony/asset": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/error-handler": "^4.4|^5.0", - "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/form": "^4.4.17", - "symfony/http-foundation": "^4.3|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/intl": "^4.4|^5.0", - "symfony/mime": "^4.3|^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/routing": "^3.4|^4.0|^5.0", - "symfony/templating": "^3.4|^4.0|^5.0", - "symfony/translation": "^4.2.1|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", "symfony/security-acl": "^2.8|^3.0", - "symfony/security-core": "^3.0|^4.0|^5.0", - "symfony/security-csrf": "^3.4|^4.0|^5.0", - "symfony/security-http": "^3.4|^4.0|^5.0", - "symfony/stopwatch": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/web-link": "^4.4|^5.0", - "symfony/workflow": "^4.3|^5.0", + "symfony/security-core": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3" }, "conflict": { - "symfony/console": "<3.4", - "symfony/form": "<4.4", - "symfony/http-foundation": "<4.3", - "symfony/translation": "<4.2", - "symfony/workflow": "<4.3" + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<5.4", + "symfony/form": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/translation": "<5.4", + "symfony/workflow": "<5.4" }, "suggest": { "symfony/finder": "", @@ -63,7 +67,6 @@ "symfony/form": "For using the FormExtension", "symfony/http-kernel": "For using the HttpKernelExtension", "symfony/routing": "For using the RoutingExtension", - "symfony/templating": "For using the TwigEngine", "symfony/translation": "For using the TranslationExtension", "symfony/yaml": "For using the YamlExtension", "symfony/security-core": "For using the SecurityExtension", diff --git a/src/Symfony/Bridge/Twig/phpunit.xml.dist b/src/Symfony/Bridge/Twig/phpunit.xml.dist index 6e1ada1b3981a..e5a59c8c5edec 100644 --- a/src/Symfony/Bridge/Twig/phpunit.xml.dist +++ b/src/Symfony/Bridge/Twig/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php b/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php index 7df85c70c90e7..cc8588f1ceb6c 100644 --- a/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php +++ b/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\DebugBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -25,6 +26,7 @@ * * @internal */ +#[AsCommand(name: 'server:dump', description: 'Start a dump server that collects and displays dumps in a single place')] class ServerDumpPlaceholderCommand extends Command { private $replacedCommand; diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php index fd7d8e72d6a4c..04fd507612747 100644 --- a/src/Symfony/Bundle/DebugBundle/DebugBundle.php +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\DebugBundle; use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; -use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\RemoveWebServerBundleLoggerPass; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -53,7 +52,6 @@ public function build(ContainerBuilder $container) parent::build($container); $container->addCompilerPass(new DumpDataCollectorPass()); - $container->addCompilerPass(new RemoveWebServerBundleLoggerPass()); } public function registerCommands(Application $application) diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php deleted file mode 100644 index 2d30cf21d9d6d..0000000000000 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\DebugBundle\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * @author Jérémy Derussé - * - * @internal - */ -final class RemoveWebServerBundleLoggerPass implements CompilerPassInterface -{ - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container) - { - if ($container->hasDefinition('web_server.command.server_log') && $container->hasDefinition('monolog.command.server_log')) { - $container->removeDefinition('web_server.command.server_log'); - } - } -} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php index e3a724cd2448a..9843893088074 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php @@ -13,7 +13,6 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -use Symfony\Component\VarDumper\Dumper\HtmlDumper; /** * DebugExtension configuration structure. @@ -25,7 +24,7 @@ class Configuration implements ConfigurationInterface /** * {@inheritdoc} */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('debug'); @@ -51,21 +50,13 @@ public function getConfigTreeBuilder() ->example('php://stderr, or tcp://%env(VAR_DUMPER_SERVER)% when using the "server:dump" command') ->defaultNull() ->end() - ->end() - ; - - if (method_exists(HtmlDumper::class, 'setTheme')) { - $rootNode - ->children() - ->enumNode('theme') - ->info('Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light"') - ->example('dark') - ->values(['dark', 'light']) - ->defaultValue('dark') - ->end() + ->enumNode('theme') + ->info('Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light"') + ->example('dark') + ->values(['dark', 'light']) + ->defaultValue('dark') ->end() ; - } return $treeBuilder; } diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index 7b8e6234709da..731cc62b3116d 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -16,7 +16,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Dumper\CliDumper; @@ -37,8 +37,8 @@ public function load(array $configs, ContainerBuilder $container) $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('services.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.php'); $container->getDefinition('var_dumper.cloner') ->addMethodCall('setMaxItems', [$config['max_items']]) @@ -100,7 +100,7 @@ public function load(array $configs, ContainerBuilder $container) /** * {@inheritdoc} */ - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string|false { return __DIR__.'/../Resources/config/schema'; } @@ -108,7 +108,7 @@ public function getXsdValidationBasePath() /** * {@inheritdoc} */ - public function getNamespace() + public function getNamespace(): string { return 'http://symfony.com/schema/dic/debug'; } diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.php b/src/Symfony/Bundle/DebugBundle/Resources/config/services.php new file mode 100644 index 0000000000000..d0f57c092872e --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Monolog\Formatter\FormatterInterface; +use Symfony\Bridge\Monolog\Command\ServerLogCommand; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; +use Symfony\Bridge\Twig\Extension\DumpExtension; +use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\HttpKernel\EventListener\DumpListener; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor; +use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor; +use Symfony\Component\VarDumper\Command\ServerDumpCommand; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider; +use Symfony\Component\VarDumper\Dumper\ContextProvider\RequestContextProvider; +use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; +use Symfony\Component\VarDumper\Dumper\ContextualizedDumper; +use Symfony\Component\VarDumper\Dumper\HtmlDumper; +use Symfony\Component\VarDumper\Server\Connection; +use Symfony\Component\VarDumper\Server\DumpServer; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('env(VAR_DUMPER_SERVER)', '127.0.0.1:9912') + ; + + $container->services() + + ->set('twig.extension.dump', DumpExtension::class) + ->args([ + service('var_dumper.cloner'), + service('var_dumper.html_dumper'), + ]) + ->tag('twig.extension') + + ->set('data_collector.dump', DumpDataCollector::class) + ->public() + ->args([ + service('debug.stopwatch')->ignoreOnInvalid(), + service('debug.file_link_formatter')->ignoreOnInvalid(), + param('kernel.charset'), + service('request_stack'), + null, // var_dumper.cli_dumper or var_dumper.server_connection when debug.dump_destination is set + ]) + ->tag('data_collector', [ + 'id' => 'dump', + 'template' => '@Debug/Profiler/dump.html.twig', + 'priority' => 240, + ]) + + ->set('debug.dump_listener', DumpListener::class) + ->args([ + service('var_dumper.cloner'), + service('var_dumper.cli_dumper'), + null, + ]) + ->tag('kernel.event_subscriber') + + ->set('var_dumper.cloner', VarCloner::class) + ->public() + + ->set('var_dumper.cli_dumper', CliDumper::class) + ->args([ + null, // debug.dump_destination, + param('kernel.charset'), + 0, // flags + ]) + + ->set('var_dumper.contextualized_cli_dumper', ContextualizedDumper::class) + ->decorate('var_dumper.cli_dumper') + ->args([ + service('var_dumper.contextualized_cli_dumper.inner'), + [ + 'source' => inline_service(SourceContextProvider::class)->args([ + param('kernel.charset'), + param('kernel.project_dir'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]), + ], + ]) + + ->set('var_dumper.html_dumper', HtmlDumper::class) + ->args([ + null, + param('kernel.charset'), + 0, // flags + ]) + ->call('setDisplayOptions', [ + ['fileLinkFormat' => service('debug.file_link_formatter')->ignoreOnInvalid()], + ]) + + ->set('var_dumper.server_connection', Connection::class) + ->args([ + '', // server host + [ + 'source' => inline_service(SourceContextProvider::class)->args([ + param('kernel.charset'), + param('kernel.project_dir'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]), + 'request' => inline_service(RequestContextProvider::class)->args([service('request_stack')]), + 'cli' => inline_service(CliContextProvider::class), + ], + ]) + + ->set('var_dumper.dump_server', DumpServer::class) + ->args([ + '', // server host + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'debug']) + + ->set('var_dumper.command.server_dump', ServerDumpCommand::class) + ->args([ + service('var_dumper.dump_server'), + [ + 'cli' => inline_service(CliDescriptor::class)->args([service('var_dumper.contextualized_cli_dumper.inner')]), + 'html' => inline_service(HtmlDescriptor::class)->args([service('var_dumper.html_dumper')]), + ], + ]) + ->tag('console.command') + + ->set('monolog.command.server_log', ServerLogCommand::class) + ; + + if (class_exists(ConsoleFormatter::class) && interface_exists(FormatterInterface::class)) { + $container->services()->get('monolog.command.server_log')->tag('console.command'); + } +}; diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml deleted file mode 100644 index c7cc5725cf8f8..0000000000000 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - 127.0.0.1:9912 - - - - - - - - - - - - - - - - %kernel.charset% - - null - - - - - - - null - - - - - null - %kernel.charset% - 0 - - - - - - - - %kernel.charset% - %kernel.project_dir% - - - - - - - - null - %kernel.charset% - 0 - - - - - - - - - - - - - %kernel.charset% - %kernel.project_dir% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php index 1f85a1a31696e..31afae4d93acb 100644 --- a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php @@ -15,7 +15,11 @@ use Symfony\Bundle\DebugBundle\DependencyInjection\DebugExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\VarDumper\Caster\ReflectionCaster; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use Symfony\Component\VarDumper\Server\Connection; +use Symfony\Component\VarDumper\Server\DumpServer; class DebugExtensionTest extends TestCase { @@ -70,12 +74,50 @@ public function testUnsetClosureFileInfoShouldBeRegisteredInVarCloner() $this->assertTrue($called); } + public function provideServicesUsingDumpDestinationCreation(): array + { + return [ + ['tcp://localhost:1234', 'tcp://localhost:1234', null], + [null, '', null], + ['php://stderr', '', 'php://stderr'], + ]; + } + + /** + * @dataProvider provideServicesUsingDumpDestinationCreation + */ + public function testServicesUsingDumpDestinationCreation(?string $dumpDestination, string $expectedHost, ?string $expectedOutput) + { + $container = $this->createContainer(); + $container->registerExtension(new DebugExtension()); + $container->loadFromExtension('debug', ['dump_destination' => $dumpDestination]); + $container->setAlias('dump_server_public', 'var_dumper.dump_server')->setPublic(true); + $container->setAlias('server_conn_public', 'var_dumper.server_connection')->setPublic(true); + $container->setAlias('cli_dumper_public', 'var_dumper.cli_dumper')->setPublic(true); + $container->register('request_stack', RequestStack::class); + $this->compileContainer($container); + + $dumpServer = $container->get('dump_server_public'); + $this->assertInstanceOf(DumpServer::class, $dumpServer); + $this->assertSame($expectedHost, $container->findDefinition('dump_server_public')->getArgument(0)); + + $serverConn = $container->get('server_conn_public'); + $this->assertInstanceOf(Connection::class, $serverConn); + $this->assertSame($expectedHost, $container->findDefinition('server_conn_public')->getArgument(0)); + + $cliDumper = $container->get('cli_dumper_public'); + $this->assertInstanceOf(CliDumper::class, $cliDumper); + $this->assertSame($expectedOutput, $container->findDefinition('cli_dumper_public')->getArgument(0)); + } + private function createContainer() { $container = new ContainerBuilder(new ParameterBag([ 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, 'kernel.charset' => 'UTF-8', 'kernel.debug' => true, + 'kernel.project_dir' => __DIR__, 'kernel.bundles' => ['DebugBundle' => 'Symfony\\Bundle\\DebugBundle\\DebugBundle'], ])); diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index 261f3d23c7264..6dc3f8c126b0a 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -16,21 +16,20 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=8.0.2", "ext-xml": "*", - "symfony/http-kernel": "^3.4|^4.0|^5.0", - "symfony/polyfill-php80": "^1.16", - "symfony/twig-bridge": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.1.1|^5.0" + "symfony/http-kernel": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" }, "require-dev": { - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/web-profiler-bundle": "^3.4|^4.0|^5.0" + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/web-profiler-bundle": "^5.4|^6.0" }, "conflict": { - "symfony/config": "<4.2", - "symfony/dependency-injection": "<3.4" + "symfony/config": "<5.4", + "symfony/dependency-injection": "<5.4" }, "suggest": { "symfony/config": "For service container configuration", diff --git a/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist b/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist index 9060c8bea7cbe..a81e38228ec4c 100644 --- a/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist +++ b/src/Symfony/Bundle/DebugBundle/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 7a7e049dcfd08..862a1ae089c7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -1,6 +1,127 @@ CHANGELOG ========= +6.0 +--- + + * Remove the `session.storage` alias and `session.storage.*` services, use the `session.storage.factory` alias and `session.storage.factory.*` services instead + * Remove `framework.session.storage_id` configuration option, use the `framework.session.storage_factory_id` configuration option instead + * Remove the `session` service and the `SessionInterface` alias, use the `\Symfony\Component\HttpFoundation\Request::getSession()` or the new `\Symfony\Component\HttpFoundation\RequestStack::getSession()` methods instead + * Remove the `session.attribute_bag` service and `session.flash_bag` service + * Remove the `lock.RESOURCE_NAME` and `lock.RESOURCE_NAME.store` services and the `lock`, `LockInterface`, `lock.store` and `PersistingStoreInterface` aliases, use `lock.RESOURCE_NAME.factory`, `lock.factory` or `LockFactory` instead + * The `form.factory`, `form.type.file`, `translator`, `security.csrf.token_manager`, `serializer`, + `cache_clearer`, `filesystem` and `validator` services are now private + * Remove the `output-format` and `xliff-version` options from `TranslationUpdateCommand` + * Remove `has()`, `get()`, `getDoctrine()`n and `dispatchMessage()` from `AbstractController`, use method/constructor injection instead + * Make the "framework.router.utf8" configuration option default to `true` + * Remove the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead + * Make the `profiler` service private + * Remove all other values than "none", "php_array" and "file" for `framework.annotation.cache` + * Register workflow services as private + * Remove support for passing a `RouteCollectionBuilder` to `MicroKernelTrait::configureRoutes()`, type-hint `RoutingConfigurator` instead + * Remove the `cache.adapter.doctrine` service + * Remove the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead + * Make the `framework.messenger.reset_on_message` configuration option default to `true` + +5.4 +--- + + * Add `set_locale_from_accept_language` config option to automatically set the request locale based on the `Accept-Language` + HTTP request header and the `framework.enabled_locales` config option + * Add `set_content_language_from_locale` config option to automatically set the `Content-Language` HTTP response header based on the Request locale + * Deprecate the `framework.translator.enabled_locales`, use `framework.enabled_locales` instead + * Add autowiring alias for `HttpCache\StoreInterface` + * Add the ability to enable the profiler using a request query parameter, body parameter or attribute + * Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead + * Deprecate the public `profiler` service to private + * Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead + * Deprecate the `cache.adapter.doctrine` service + * Add support for resetting container services after each messenger message + * Add `configureContainer()`, `configureRoutes()`, `getConfigDir()` and `getBundlesPath()` to `MicroKernelTrait` + * Add support for configuring log level, and status code by exception class + * Bind the `default_context` parameter onto serializer's encoders and normalizers + * Add support for `statusCode` default parameter when loading a template directly from route using the `Symfony\Bundle\FrameworkBundle\Controller\TemplateController` controller + * Deprecate `translation:update` command, use `translation:extract` instead + * Add `PhpStanExtractor` support for the PropertyInfo component + * Add `cache.adapter.doctrine_dbal` service to replace `cache.adapter.pdo` when a Doctrine DBAL connection is used. + +5.3 +--- + + * Deprecate the `session.storage` alias and `session.storage.*` services, use the `session.storage.factory` alias and `session.storage.factory.*` services instead + * Deprecate the `framework.session.storage_id` configuration option, use the `framework.session.storage_factory_id` configuration option instead + * Deprecate the `session` service and the `SessionInterface` alias, use the `Request::getSession()` or the new `RequestStack::getSession()` methods instead + * Add `AbstractController::renderForm()` to render a form and set the appropriate HTTP status code + * Add support for configuring PHP error level to log levels + * Add the `dispatcher` option to `debug:event-dispatcher` + * Add the `event_dispatcher.dispatcher` tag + * Add `assertResponseFormatSame()` in `BrowserKitAssertionsTrait` + * Add support for configuring UUID factory services + * Add tag `assets.package` to register asset packages + * Add support to use a PSR-6 compatible cache for Doctrine annotations + * Deprecate all other values than "none", "php_array" and "file" for `framework.annotation.cache` + * Add `KernelTestCase::getContainer()` as the best way to get a container in tests + * Rename the container parameter `profiler_listener.only_master_requests` to `profiler_listener.only_main_requests` + * Add service `fragment.uri_generator` to generate the URI of a fragment + * Deprecate registering workflow services as public + * Deprecate option `--xliff-version` of the `translation:update` command, use e.g. `--format=xlf20` instead + * Deprecate option `--output-format` of the `translation:update` command, use e.g. `--format=xlf20` instead + +5.2.0 +----- + + * Added `framework.http_cache` configuration tree + * Added `framework.trusted_proxies` and `framework.trusted_headers` configuration options + * Deprecated the public `form.factory`, `form.type.file`, `translator`, `security.csrf.token_manager`, `serializer`, + `cache_clearer`, `filesystem` and `validator` services to private. + * Added `TemplateAwareDataCollectorInterface` and `AbstractDataCollector` to simplify custom data collector creation and leverage autoconfiguration + * Add `cache.adapter.redis_tag_aware` tag to use `RedisCacheAwareAdapter` + * added `framework.http_client.retry_failing` configuration tree + * added `assertCheckboxChecked()` and `assertCheckboxNotChecked()` in `WebTestCase` + * added `assertFormValue()` and `assertNoFormValue()` in `WebTestCase` + * Added "--as-tree=3" option to `translation:update` command to dump messages as a tree-like structure. The given value defines the level where to switch to inline YAML + * Deprecated the `lock.RESOURCE_NAME` and `lock.RESOURCE_NAME.store` services and the `lock`, `LockInterface`, `lock.store` and `PersistingStoreInterface` aliases, use `lock.RESOURCE_NAME.factory`, `lock.factory` or `LockFactory` instead. + +5.1.0 +----- + * Removed `--no-backup` option from `translation:update` command (broken since `5.0.0`) + * Added link to source for controllers registered as named services + * Added link to source on controller on `router:match`/`debug:router` (when `framework.ide` is configured) + * Added the `framework.router.default_uri` configuration option to configure the default `RequestContext` + * Made `MicroKernelTrait::configureContainer()` compatible with `ContainerConfigurator` + * Added a new `mailer.message_bus` option to configure or disable the message bus to use to send mails. + * Added flex-compatible default implementation for `MicroKernelTrait::registerBundles()` + * Deprecated passing a `RouteCollectionBuilder` to `MicroKernelTrait::configureRoutes()`, type-hint `RoutingConfigurator` instead + * The `TemplateController` now accepts context argument + * Deprecated *not* setting the "framework.router.utf8" configuration option as it will default to `true` in Symfony 6.0 + * Added tag `routing.expression_language_function` to define functions available in route conditions + * Added `debug:container --deprecations` option to see compile-time deprecations. + * Made `BrowserKitAssertionsTrait` report the original error message in case of a failure + * Added ability for `config:dump-reference` and `debug:config` to dump and debug kernel container extension configuration. + * Deprecated `session.attribute_bag` service and `session.flash_bag` service. + +5.0.0 +----- + + * Removed support to load translation resources from the legacy directories `src/Resources/translations/` and `src/Resources//translations/` + * Removed `ControllerNameParser`. + * Removed `ResolveControllerNameSubscriber` + * Removed support for `bundle:controller:action` to reference controllers. Use `serviceOrFqcn::method` instead + * Removed support for PHP templating, use Twig instead + * Removed `Controller`, use `AbstractController` instead + * Removed `Client`, use `KernelBrowser` instead + * Removed `ContainerAwareCommand`, use dependency injection instead + * Removed the `validation.strict_email` option, use `validation.email_validation_mode` instead + * Removed the `cache.app.simple` service and its corresponding PSR-16 autowiring alias + * Removed cache-related compiler passes and `RequestDataCollector` + * Removed the `translator.selector` and `session.save_listener` services + * Removed `SecurityUserValueResolver`, use `UserValueResolver` instead + * Removed `routing.loader.service`. + * Service route loaders must be tagged with `routing.route_loader`. + * Added `slugger` service and `SluggerInterface` alias + * Removed the `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract` services. + * Removed the `router.cache_class_prefix` parameter. + 4.4.0 ----- @@ -22,6 +143,7 @@ CHANGELOG * Made `framework.session.handler_id` accept a DSN * Marked the `RouterDataCollector` class as `@final`. * [BC Break] The `framework.messenger.buses..middleware` config key is not deeply merged anymore. + * Moved `MailerAssertionsTrait` in `KernelTestCase` 4.3.0 ----- @@ -47,8 +169,8 @@ CHANGELOG options if you're using Symfony's serializer. * [BC Break] Removed the `framework.messenger.routing.send_and_handle` configuration. Instead of setting it to true, configure a `SyncTransport` and route messages to it. - * Added information about deprecated aliases in `debug:autowiring` - * Added php ini session options `sid_length` and `sid_bits_per_character` + * Added information about deprecated aliases in `debug:autowiring` + * Added php ini session options `sid_length` and `sid_bits_per_character` to the `session` section of the configuration * Added support for Translator paths, Twig paths in translation commands. * Added support for PHP files with translations in translation commands. diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php index f1b94bdfcdf91..097a356008a5b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php @@ -19,7 +19,7 @@ abstract class AbstractPhpFileCacheWarmer implements CacheWarmerInterface { - private $phpArrayFile; + private string $phpArrayFile; /** * @param string $phpArrayFile The PHP file where metadata are cached @@ -32,22 +32,24 @@ public function __construct(string $phpArrayFile) /** * {@inheritdoc} */ - public function isOptional() + public function isOptional(): bool { return true; } /** * {@inheritdoc} + * + * @return string[] A list of classes to preload on PHP 7.4+ */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir): array { $arrayAdapter = new ArrayAdapter(); spl_autoload_register([ClassExistenceResource::class, 'throwOnRequiredClass']); try { if (!$this->doWarmUp($cacheDir, $arrayAdapter)) { - return; + return []; } } finally { spl_autoload_unregister([ClassExistenceResource::class, 'throwOnRequiredClass']); @@ -58,12 +60,15 @@ public function warmUp($cacheDir) // so here we un-serialize the values first $values = array_map(function ($val) { return null !== $val ? unserialize($val) : null; }, $arrayAdapter->getValues()); - $this->warmUpPhpArrayAdapter(new PhpArrayAdapter($this->phpArrayFile, new NullAdapter()), $values); + return $this->warmUpPhpArrayAdapter(new PhpArrayAdapter($this->phpArrayFile, new NullAdapter()), $values); } - protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) + /** + * @return string[] A list of classes to preload on PHP 7.4+ + */ + protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values): array { - $phpArrayAdapter->warmUp($values); + return (array) $phpArrayAdapter->warmUp($values); } /** @@ -78,9 +83,7 @@ final protected function ignoreAutoloadException(string $class, \Exception $exce } /** - * @param string $cacheDir - * * @return bool false if there is nothing to warm-up */ - abstract protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter); + abstract protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter): bool; } diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php index 8ed3f618f902a..e10471617b2e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php @@ -12,13 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Doctrine\Common\Annotations\AnnotationException; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; -use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\DoctrineProvider; /** * Warms up annotation caches for classes found in composer's autoload class map @@ -29,21 +26,14 @@ class AnnotationsCacheWarmer extends AbstractPhpFileCacheWarmer { private $annotationReader; - private $excludeRegexp; - private $debug; + private ?string $excludeRegexp; + private bool $debug; /** - * @param string $phpArrayFile The PHP file where annotations are cached - * @param string $excludeRegexp - * @param bool $debug + * @param string $phpArrayFile The PHP file where annotations are cached */ - public function __construct(Reader $annotationReader, string $phpArrayFile, $excludeRegexp = null, $debug = false) + public function __construct(Reader $annotationReader, string $phpArrayFile, string $excludeRegexp = null, bool $debug = false) { - if ($excludeRegexp instanceof CacheItemPoolInterface) { - @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), \E_USER_DEPRECATED); - $excludeRegexp = $debug; - $debug = 4 < \func_num_args() && func_get_arg(4); - } parent::__construct($phpArrayFile); $this->annotationReader = $annotationReader; $this->excludeRegexp = $excludeRegexp; @@ -53,7 +43,7 @@ public function __construct(Reader $annotationReader, string $phpArrayFile, $exc /** * {@inheritdoc} */ - protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) + protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter): bool { $annotatedClassPatterns = $cacheDir.'/annotations.map'; @@ -62,10 +52,7 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) } $annotatedClasses = include $annotatedClassPatterns; - $reader = class_exists(PsrCachedReader::class) - ? new PsrCachedReader($this->annotationReader, $arrayAdapter, $this->debug) - : new CachedReader($this->annotationReader, new DoctrineProvider($arrayAdapter), $this->debug) - ; + $reader = new PsrCachedReader($this->annotationReader, $arrayAdapter, $this->debug); foreach ($annotatedClasses as $class) { if (null !== $this->excludeRegexp && preg_match($this->excludeRegexp, $class)) { @@ -81,12 +68,15 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) return true; } - protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) + /** + * @return string[] A list of classes to preload on PHP 7.4+ + */ + protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values): array { // make sure we don't cache null values $values = array_filter($values, function ($val) { return null !== $val; }); - parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values); + return parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values); } private function readAllComponents(Reader $reader, string $class) diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php new file mode 100644 index 0000000000000..cd184cf64762c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/CachePoolClearerCacheWarmer.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; + +use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; + +/** + * Clears the cache pools when warming up the cache. + * + * Do not use in production! + * + * @author Teoh Han Hui + * + * @internal + */ +final class CachePoolClearerCacheWarmer implements CacheWarmerInterface +{ + private $poolClearer; + private array $pools; + + /** + * @param string[] $pools + */ + public function __construct(Psr6CacheClearer $poolClearer, array $pools = []) + { + $this->poolClearer = $poolClearer; + $this->pools = $pools; + } + + /** + * {@inheritdoc} + * + * @return string[] + */ + public function warmUp(string $cacheDirectory): array + { + foreach ($this->pools as $pool) { + if ($this->poolClearer->hasPool($pool)) { + $this->poolClearer->clearPool($pool); + } + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function isOptional(): bool + { + // optional cache warmers are not run when handling the request + return false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php new file mode 100644 index 0000000000000..c0d96c5ea7b74 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Config\Builder\ConfigBuilderGenerator; +use Symfony\Component\Config\Builder\ConfigBuilderGeneratorInterface; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * Generate all config builders. + * + * @author Tobias Nyholm + */ +class ConfigBuilderCacheWarmer implements CacheWarmerInterface +{ + private $kernel; + private $logger; + + public function __construct(KernelInterface $kernel, LoggerInterface $logger = null) + { + $this->kernel = $kernel; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + * + * @return string[] + */ + public function warmUp(string $cacheDir): array + { + $generator = new ConfigBuilderGenerator($cacheDir); + + foreach ($this->kernel->getBundles() as $bundle) { + $extension = $bundle->getContainerExtension(); + if (null === $extension) { + continue; + } + + try { + $this->dumpExtension($extension, $generator); + } catch (\Exception $e) { + if ($this->logger) { + $this->logger->warning('Failed to generate ConfigBuilder for extension {extensionClass}.', ['exception' => $e, 'extensionClass' => \get_class($extension)]); + } + } + } + + // No need to preload anything + return []; + } + + private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGeneratorInterface $generator): void + { + $configuration = null; + if ($extension instanceof ConfigurationInterface) { + $configuration = $extension; + } elseif ($extension instanceof ConfigurationExtensionInterface) { + $configuration = $extension->getConfiguration([], new ContainerBuilder($this->kernel->getContainer()->getParameterBag())); + } + + if (!$configuration) { + return; + } + + $generator->build($configuration); + } + + /** + * {@inheritdoc} + */ + public function isOptional(): bool + { + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php index 61d9f21a5aaf5..6cdf176bb33bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php @@ -12,10 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Psr\Container\ContainerInterface; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\CompatibilityServiceSubscriberInterface as ServiceSubscriberInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Routing\RouterInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; /** * Generates the router matcher and generator classes. @@ -35,27 +35,21 @@ public function __construct(ContainerInterface $container) } /** - * Warms up the cache. - * - * @param string $cacheDir The cache directory + * {@inheritdoc} */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir): array { $router = $this->container->get('router'); if ($router instanceof WarmableInterface) { - $router->warmUp($cacheDir); - - return; + return (array) $router->warmUp($cacheDir); } - @trigger_error(sprintf('Passing a %s without implementing %s is deprecated since Symfony 4.1.', RouterInterface::class, WarmableInterface::class), \E_USER_DEPRECATED); + throw new \LogicException(sprintf('The router "%s" cannot be warmed up because it does not implement "%s".', get_debug_type($router), WarmableInterface::class)); } /** - * Checks whether this warmer is optional or not. - * - * @return bool always true + * {@inheritdoc} */ public function isOptional(): bool { diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php index c73203927db83..4aae3ba96eaf5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Doctrine\Common\Annotations\AnnotationException; -use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -28,7 +27,7 @@ */ class SerializerCacheWarmer extends AbstractPhpFileCacheWarmer { - private $loaders; + private array $loaders; /** * @param LoaderInterface[] $loaders The serializer metadata loaders @@ -36,9 +35,6 @@ class SerializerCacheWarmer extends AbstractPhpFileCacheWarmer */ public function __construct(array $loaders, string $phpArrayFile) { - if (2 < \func_num_args() && func_get_arg(2) instanceof CacheItemPoolInterface) { - @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), \E_USER_DEPRECATED); - } parent::__construct($phpArrayFile); $this->loaders = $loaders; } @@ -46,7 +42,7 @@ public function __construct(array $loaders, string $phpArrayFile) /** * {@inheritdoc} */ - protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) + protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter): bool { if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { return false; diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php deleted file mode 100644 index 80099b44ae58a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; - -@trigger_error('The '.TemplateFinder::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Finder\Finder; -use Symfony\Component\HttpKernel\Bundle\BundleInterface; -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReferenceInterface; - -/** - * Finds all the templates. - * - * @author Victor Berchet - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplateFinder implements TemplateFinderInterface -{ - private $kernel; - private $parser; - private $rootDir; - private $templates; - - /** - * @param string $rootDir The directory where global templates can be stored - */ - public function __construct(KernelInterface $kernel, TemplateNameParserInterface $parser, string $rootDir) - { - $this->kernel = $kernel; - $this->parser = $parser; - $this->rootDir = $rootDir; - } - - /** - * Find all the templates in the bundle and in the kernel Resources folder. - * - * @return TemplateReferenceInterface[] - */ - public function findAllTemplates() - { - if (null !== $this->templates) { - return $this->templates; - } - - $templates = []; - - foreach ($this->kernel->getBundles() as $bundle) { - $templates = array_merge($templates, $this->findTemplatesInBundle($bundle)); - } - - $templates = array_merge($templates, $this->findTemplatesInFolder($this->rootDir.'/views')); - - return $this->templates = $templates; - } - - /** - * Find templates in the given directory. - * - * @return TemplateReferenceInterface[] - */ - private function findTemplatesInFolder(string $dir): array - { - $templates = []; - - if (is_dir($dir)) { - $finder = new Finder(); - foreach ($finder->files()->followLinks()->in($dir) as $file) { - $template = $this->parser->parse($file->getRelativePathname()); - if (false !== $template) { - $templates[] = $template; - } - } - } - - return $templates; - } - - /** - * Find templates in the given bundle. - * - * @param BundleInterface $bundle The bundle where to look for templates - * - * @return TemplateReferenceInterface[] - */ - private function findTemplatesInBundle(BundleInterface $bundle): array - { - $name = $bundle->getName(); - $templates = array_unique(array_merge( - $this->findTemplatesInFolder($bundle->getPath().'/Resources/views'), - $this->findTemplatesInFolder($this->rootDir.'/'.$name.'/views') - )); - - foreach ($templates as $i => $template) { - $templates[$i] = $template->set('bundle', $name); - } - - return $templates; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php deleted file mode 100644 index 51dc25942d3ae..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; - -@trigger_error('The '.TemplateFinderInterface::class.' interface is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -/** - * Interface for finding all the templates. - * - * @author Victor Berchet - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -interface TemplateFinderInterface -{ - /** - * Find all the templates. - * - * @return array An array of templates of type TemplateReferenceInterface - */ - public function findAllTemplates(); -} diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php deleted file mode 100644 index 80deb31069835..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; - -@trigger_error('The '.TemplatePathsCacheWarmer::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Bundle\FrameworkBundle\Templating\Loader\TemplateLocator; -use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer; - -/** - * Computes the association between template names and their paths on the disk. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplatePathsCacheWarmer extends CacheWarmer -{ - protected $finder; - protected $locator; - - public function __construct(TemplateFinderInterface $finder, TemplateLocator $locator) - { - $this->finder = $finder; - $this->locator = $locator; - } - - /** - * Warms up the cache. - * - * @param string $cacheDir The cache directory - */ - public function warmUp($cacheDir) - { - $filesystem = new Filesystem(); - $templates = []; - - foreach ($this->finder->findAllTemplates() as $template) { - $templates[$template->getLogicalName()] = rtrim($filesystem->makePathRelative($this->locator->locate($template), $cacheDir), '/'); - } - - $templates = str_replace("' => '", "' => __DIR__.'/", var_export($templates, true)); - - $this->writeCacheFile($cacheDir.'/templates.php', sprintf("translator) { - $this->translator = $this->container->get('translator'); - } + $this->translator ??= $this->container->get('translator'); if ($this->translator instanceof WarmableInterface) { - $this->translator->warmUp($cacheDir); + return (array) $this->translator->warmUp($cacheDir); } + + return []; } /** * {@inheritdoc} */ - public function isOptional() + public function isOptional(): bool { return true; } @@ -58,7 +60,7 @@ public function isOptional() /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'translator' => TranslatorInterface::class, diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php index ec2c8df4ae5eb..6b2c558be96e5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; use Doctrine\Common\Annotations\AnnotationException; -use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; @@ -21,7 +20,6 @@ use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\ValidatorBuilder; -use Symfony\Component\Validator\ValidatorBuilderInterface; /** * Warms up XML and YAML validator metadata. @@ -33,17 +31,10 @@ class ValidatorCacheWarmer extends AbstractPhpFileCacheWarmer private $validatorBuilder; /** - * @param ValidatorBuilder $validatorBuilder - * @param string $phpArrayFile The PHP file where metadata are cached + * @param string $phpArrayFile The PHP file where metadata are cached */ - public function __construct($validatorBuilder, string $phpArrayFile) + public function __construct(ValidatorBuilder $validatorBuilder, string $phpArrayFile) { - if (!$validatorBuilder instanceof ValidatorBuilder && !$validatorBuilder instanceof ValidatorBuilderInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, ValidatorBuilder::class, \is_object($validatorBuilder) ? \get_class($validatorBuilder) : \gettype($validatorBuilder))); - } - if (2 < \func_num_args() && func_get_arg(2) instanceof CacheItemPoolInterface) { - @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), \E_USER_DEPRECATED); - } parent::__construct($phpArrayFile); $this->validatorBuilder = $validatorBuilder; } @@ -51,7 +42,7 @@ public function __construct($validatorBuilder, string $phpArrayFile) /** * {@inheritdoc} */ - protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) + protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter): bool { if (!method_exists($this->validatorBuilder, 'getLoaders')) { return false; @@ -77,12 +68,15 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) return true; } - protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) + /** + * @return string[] A list of classes to preload on PHP 7.4+ + */ + protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values): array { // make sure we don't cache null values $values = array_filter($values, function ($val) { return null !== $val; }); - parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values); + return parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Client.php b/src/Symfony/Bundle/FrameworkBundle/Client.php deleted file mode 100644 index 97b19cce3a499..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Client.php +++ /dev/null @@ -1,207 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle; - -use Symfony\Component\BrowserKit\CookieJar; -use Symfony\Component\BrowserKit\History; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\HttpKernelBrowser; -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile; - -/** - * Client simulates a browser and makes requests to a Kernel object. - * - * @deprecated since Symfony 4.3, use KernelBrowser instead. - */ -class Client extends HttpKernelBrowser -{ - private $hasPerformedRequest = false; - private $profiler = false; - private $reboot = true; - - /** - * {@inheritdoc} - */ - public function __construct(KernelInterface $kernel, array $server = [], History $history = null, CookieJar $cookieJar = null) - { - parent::__construct($kernel, $server, $history, $cookieJar); - } - - /** - * Returns the container. - * - * @return ContainerInterface|null Returns null when the Kernel has been shutdown or not started yet - */ - public function getContainer() - { - return $this->kernel->getContainer(); - } - - /** - * Returns the kernel. - * - * @return KernelInterface - */ - public function getKernel() - { - return $this->kernel; - } - - /** - * Gets the profile associated with the current Response. - * - * @return HttpProfile|false|null A Profile instance - */ - public function getProfile() - { - if (null === $this->response || !$this->kernel->getContainer()->has('profiler')) { - return false; - } - - return $this->kernel->getContainer()->get('profiler')->loadProfileFromResponse($this->response); - } - - /** - * Enables the profiler for the very next request. - * - * If the profiler is not enabled, the call to this method does nothing. - */ - public function enableProfiler() - { - if ($this->kernel->getContainer()->has('profiler')) { - $this->profiler = true; - } - } - - /** - * Disables kernel reboot between requests. - * - * By default, the Client reboots the Kernel for each request. This method - * allows to keep the same kernel across requests. - */ - public function disableReboot() - { - $this->reboot = false; - } - - /** - * Enables kernel reboot between requests. - */ - public function enableReboot() - { - $this->reboot = true; - } - - /** - * {@inheritdoc} - * - * @param Request $request A Request instance - * - * @return Response A Response instance - */ - protected function doRequest($request) - { - // avoid shutting down the Kernel if no request has been performed yet - // WebTestCase::createClient() boots the Kernel but do not handle a request - if ($this->hasPerformedRequest && $this->reboot) { - $this->kernel->boot(); - $this->kernel->shutdown(); - } else { - $this->hasPerformedRequest = true; - } - - if ($this->profiler) { - $this->profiler = false; - - $this->kernel->boot(); - $this->kernel->getContainer()->get('profiler')->enable(); - } - - return parent::doRequest($request); - } - - /** - * {@inheritdoc} - * - * @param Request $request A Request instance - * - * @return Response A Response instance - */ - protected function doRequestInProcess($request) - { - $response = parent::doRequestInProcess($request); - - $this->profiler = false; - - return $response; - } - - /** - * Returns the script to execute when the request must be insulated. - * - * It assumes that the autoloader is named 'autoload.php' and that it is - * stored in the same directory as the kernel (this is the case for the - * Symfony Standard Edition). If this is not your case, create your own - * client and override this method. - * - * @param Request $request A Request instance - * - * @return string The script content - */ - protected function getScript($request) - { - $kernel = var_export(serialize($this->kernel), true); - $request = var_export(serialize($request), true); - $errorReporting = error_reporting(); - - $requires = ''; - foreach (get_declared_classes() as $class) { - if (str_starts_with($class, 'ComposerAutoloaderInit')) { - $r = new \ReflectionClass($class); - $file = \dirname($r->getFileName(), 2).'/autoload.php'; - if (file_exists($file)) { - $requires .= 'require_once '.var_export($file, true).";\n"; - } - } - } - - if (!$requires) { - throw new \RuntimeException('Composer autoloader not found.'); - } - - $requires .= 'require_once '.var_export((new \ReflectionObject($this->kernel))->getFileName(), true).";\n"; - - $profilerCode = ''; - if ($this->profiler) { - $profilerCode = '$kernel->getContainer()->get(\'profiler\')->enable();'; - } - - $code = <<boot(); -$profilerCode - -\$request = unserialize($request); -EOF; - - return $code.$this->getHandleScript(); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php index 8c454cbded9b3..8057460738f00 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\TableSeparator; @@ -27,25 +28,20 @@ * * @final */ +#[AsCommand(name: 'about', description: 'Display information about the current project')] class AboutCommand extends Command { - protected static $defaultName = 'about'; - /** * {@inheritdoc} */ protected function configure() { $this - ->setDescription('Display information about the current project') ->setHelp(<<<'EOT' The %command.name% command displays information about the current Symfony project. The PHP section displays important configuration that could affect your application. The values might be different between web and CLI. - -The Environment section displays the current environment variables managed by Symfony Dotenv. It will not -be shown if no variables were found. The values might be different between web and CLI. EOT ) ; @@ -61,13 +57,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var KernelInterface $kernel */ $kernel = $this->getApplication()->getKernel(); + if (method_exists($kernel, 'getBuildDir')) { + $buildDir = $kernel->getBuildDir(); + } else { + $buildDir = $kernel->getCacheDir(); + } + $rows = [ ['Symfony'], new TableSeparator(), ['Version', Kernel::VERSION], ['Long-Term Support', 4 === Kernel::MINOR_VERSION ? 'Yes' : 'No'], - ['End of maintenance', Kernel::END_OF_MAINTENANCE.(self::isExpired(Kernel::END_OF_MAINTENANCE) ? ' Expired' : '')], - ['End of life', Kernel::END_OF_LIFE.(self::isExpired(Kernel::END_OF_LIFE) ? ' Expired' : '')], + ['End of maintenance', Kernel::END_OF_MAINTENANCE.(self::isExpired(Kernel::END_OF_MAINTENANCE) ? ' Expired' : ' ('.self::daysBeforeExpiration(Kernel::END_OF_MAINTENANCE).')')], + ['End of life', Kernel::END_OF_LIFE.(self::isExpired(Kernel::END_OF_LIFE) ? ' Expired' : ' ('.self::daysBeforeExpiration(Kernel::END_OF_LIFE).')')], new TableSeparator(), ['Kernel'], new TableSeparator(), @@ -76,6 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['Debug', $kernel->isDebug() ? 'true' : 'false'], ['Charset', $kernel->getCharset()], ['Cache directory', self::formatPath($kernel->getCacheDir(), $kernel->getProjectDir()).' ('.self::formatFileSize($kernel->getCacheDir()).')'], + ['Build directory', self::formatPath($buildDir, $kernel->getProjectDir()).' ('.self::formatFileSize($buildDir).')'], ['Log directory', self::formatPath($kernel->getLogDir(), $kernel->getProjectDir()).' ('.self::formatFileSize($kernel->getLogDir()).')'], new TableSeparator(), ['PHP'], @@ -89,16 +92,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['Xdebug', \extension_loaded('xdebug') ? 'true' : 'false'], ]; - if ($dotenv = self::getDotenvVars()) { - $rows = array_merge($rows, [ - new TableSeparator(), - ['Environment (.env)'], - new TableSeparator(), - ], array_map(function ($value, $name) { - return [$name, $value]; - }, $dotenv, array_keys($dotenv))); - } - $io->table([], $rows); return 0; @@ -132,15 +125,10 @@ private static function isExpired(string $date): bool return false !== $date && new \DateTime() > $date->modify('last day of this month 23:59:59'); } - private static function getDotenvVars(): array + private static function daysBeforeExpiration(string $date): string { - $vars = []; - foreach (explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '') as $name) { - if ('' !== $name && isset($_ENV[$name])) { - $vars[$name] = $_ENV[$name]; - } - } + $date = \DateTime::createFromFormat('d/m/Y', '01/'.$date); - return $vars; + return (new \DateTime())->diff($date->modify('last day of this month 23:59:59'))->format('in %R%a days'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php index 530cf95d2e488..41d3eddd9964e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\StyleInterface; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; /** @@ -27,10 +28,7 @@ */ abstract class AbstractConfigCommand extends ContainerDebugCommand { - /** - * @param OutputInterface|StyleInterface $output - */ - protected function listBundles($output) + protected function listBundles(OutputInterface|StyleInterface $output) { $title = 'Available registered bundles with their extension alias if available'; $headers = ['Bundle name', 'Extension alias']; @@ -56,14 +54,27 @@ protected function listBundles($output) } } - /** - * @return ExtensionInterface - */ - protected function findExtension($name) + protected function findExtension(string $name): ExtensionInterface { $bundles = $this->initializeBundles(); $minScore = \INF; + $kernel = $this->getApplication()->getKernel(); + if ($kernel instanceof ExtensionInterface && ($kernel instanceof ConfigurationInterface || $kernel instanceof ConfigurationExtensionInterface)) { + if ($name === $kernel->getAlias()) { + return $kernel; + } + + if ($kernel->getAlias()) { + $distance = levenshtein($name, $kernel->getAlias()); + + if ($distance < $minScore) { + $guess = $kernel->getAlias(); + $minScore = $distance; + } + } + } + foreach ($bundles as $bundle) { if ($name === $bundle->getName()) { if (!$bundle->getContainerExtension()) { @@ -109,14 +120,14 @@ protected function findExtension($name) throw new LogicException($message); } - public function validateConfiguration(ExtensionInterface $extension, $configuration) + public function validateConfiguration(ExtensionInterface $extension, mixed $configuration) { if (!$configuration) { throw new \LogicException(sprintf('The extension with alias "%s" does not have its getConfiguration() method setup.', $extension->getAlias())); } if (!$configuration instanceof ConfigurationInterface) { - throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable.', \get_class($configuration))); + throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable.', get_debug_type($configuration))); } } @@ -124,8 +135,9 @@ private function initializeBundles() { // Re-build bundle manually to initialize DI extensions that can be extended by other bundles in their build() method // as this method is not called when the container is loaded from the cache. - $container = $this->getContainerBuilder(); - $bundles = $this->getApplication()->getKernel()->getBundles(); + $kernel = $this->getApplication()->getKernel(); + $container = $this->getContainerBuilder($kernel); + $bundles = $kernel->getBundles(); foreach ($bundles as $bundle) { if ($extension = $bundle->getContainerExtension()) { $container->registerExtension($extension); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index c3e1456b3863d..a46a0005dca40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; @@ -33,25 +34,20 @@ * * @final */ +#[AsCommand(name: 'assets:install', description: 'Install bundle\'s web assets under a public directory')] class AssetsInstallCommand extends Command { public const METHOD_COPY = 'copy'; public const METHOD_ABSOLUTE_SYMLINK = 'absolute symlink'; public const METHOD_RELATIVE_SYMLINK = 'relative symlink'; - protected static $defaultName = 'assets:install'; - private $filesystem; - private $projectDir; + private string $projectDir; - public function __construct(Filesystem $filesystem, string $projectDir = null) + public function __construct(Filesystem $filesystem, string $projectDir) { parent::__construct(); - if (null === $projectDir) { - @trigger_error(sprintf('Not passing the project directory to the constructor of %s is deprecated since Symfony 4.3 and will not be supported in 5.0.', __CLASS__), \E_USER_DEPRECATED); - } - $this->filesystem = $filesystem; $this->projectDir = $projectDir; } @@ -68,7 +64,6 @@ protected function configure() ->addOption('symlink', null, InputOption::VALUE_NONE, 'Symlink the assets instead of copying them') ->addOption('relative', null, InputOption::VALUE_NONE, 'Make relative symlinks') ->addOption('no-cleanup', null, InputOption::VALUE_NONE, 'Do not remove the assets of the bundles that no longer exist') - ->setDescription('Install bundle\'s web assets under a public directory') ->setHelp(<<<'EOT' The %command.name% command installs bundle assets into a given directory (e.g. the public directory). @@ -235,7 +230,7 @@ private function absoluteSymlinkWithFallback(string $originDir, string $targetDi /** * Creates symbolic link. * - * @throws IOException if link can not be created + * @throws IOException if link cannot be created */ private function symlink(string $originDir, string $targetDir, bool $relative = false) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php new file mode 100644 index 0000000000000..785027dbc8d4e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * @internal + * + * @author Robin Chalas + * @author Nicolas Grekas + */ +trait BuildDebugContainerTrait +{ + protected $containerBuilder; + + /** + * Loads the ContainerBuilder from the cache. + * + * @throws \LogicException + */ + protected function getContainerBuilder(KernelInterface $kernel): ContainerBuilder + { + if ($this->containerBuilder) { + return $this->containerBuilder; + } + + if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { + $buildContainer = \Closure::bind(function () { + $this->initializeBundles(); + + return $this->buildContainer(); + }, $kernel, \get_class($kernel)); + $container = $buildContainer(); + $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); + $container->compile(); + } else { + (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); + $locatorPass = new ServiceLocatorTagPass(); + $locatorPass->process($container); + + $container->getCompilerPassConfig()->setBeforeOptimizationPasses([]); + $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setBeforeRemovingPasses([]); + } + + return $this->containerBuilder = $container; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index d112c9b086c0f..d9b2730506020 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -11,12 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Dumper\Preloader; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; @@ -32,10 +34,9 @@ * * @final */ +#[AsCommand(name: 'cache:clear', description: 'Clear the cache')] class CacheClearCommand extends Command { - protected static $defaultName = 'cache:clear'; - private $cacheClearer; private $filesystem; @@ -57,7 +58,6 @@ protected function configure() new InputOption('no-warmup', '', InputOption::VALUE_NONE, 'Do not warm up the cache'), new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'), ]) - ->setDescription('Clear the cache') ->setHelp(<<<'EOF' The %command.name% command clears and warms up the application cache for a given environment and debug mode: @@ -79,6 +79,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $kernel = $this->getApplication()->getKernel(); $realCacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir'); + $realBuildDir = $kernel->getContainer()->hasParameter('kernel.build_dir') ? $kernel->getContainer()->getParameter('kernel.build_dir') : $realCacheDir; // the old cache dir name must not be longer than the real one to avoid exceeding // the maximum length of a directory or file path within it (esp. Windows MAX_PATH) $oldCacheDir = substr($realCacheDir, 0, -1).(str_ends_with($realCacheDir, '~') ? '+' : '~'); @@ -88,7 +89,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realCacheDir)); } + $useBuildDir = $realBuildDir !== $realCacheDir; + $oldBuildDir = substr($realBuildDir, 0, -1).('~' === substr($realBuildDir, -1) ? '+' : '~'); + if ($useBuildDir) { + $fs->remove($oldBuildDir); + + if (!is_writable($realBuildDir)) { + throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realBuildDir)); + } + + if ($this->isNfs($realCacheDir)) { + $fs->remove($realCacheDir); + } else { + $fs->rename($realCacheDir, $oldCacheDir); + } + $fs->mkdir($realCacheDir); + } + $io->comment(sprintf('Clearing the cache for the %s environment with debug %s', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); + if ($useBuildDir) { + $this->cacheClearer->clear($realBuildDir); + } $this->cacheClearer->clear($realCacheDir); // The current event dispatcher is stale, let's not use it anymore @@ -99,7 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // the warmup cache dir name must have the same length as the real one // to avoid the many problems in serialized resources files - $warmupDir = substr($realCacheDir, 0, -1).('_' === substr($realCacheDir, -1) ? '-' : '_'); + $warmupDir = substr($realBuildDir, 0, -1).('_' === substr($realBuildDir, -1) ? '-' : '_'); if ($output->isVerbose() && $fs->exists($warmupDir)) { $io->comment('Clearing outdated warmup directory...'); @@ -117,7 +138,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $warmer = $kernel->getContainer()->get('cache_warmer'); // non optional warmers already ran during container compilation $warmer->enableOnlyOptionalWarmers(); - $warmer->warmUp($realCacheDir); + $preload = (array) $warmer->warmUp($realCacheDir); + + if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { + Preloader::append($preloadFile, $preload); + } } } else { $fs->mkdir($warmupDir); @@ -130,35 +155,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if (!$fs->exists($warmupDir.'/'.$containerDir)) { - $fs->rename($realCacheDir.'/'.$containerDir, $warmupDir.'/'.$containerDir); + $fs->rename($realBuildDir.'/'.$containerDir, $warmupDir.'/'.$containerDir); touch($warmupDir.'/'.$containerDir.'.legacy'); } - if ('/' === \DIRECTORY_SEPARATOR && $mounts = @file('/proc/mounts')) { - foreach ($mounts as $mount) { - $mount = \array_slice(explode(' ', $mount), 1, -3); - if (!\in_array(array_pop($mount), ['vboxsf', 'nfs'])) { - continue; - } - $mount = implode(' ', $mount).'/'; - - if (str_starts_with($realCacheDir, $mount)) { - $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.'); - $oldCacheDir = false; - break; - } - } - } - - if ($oldCacheDir) { - $fs->rename($realCacheDir, $oldCacheDir); + if ($this->isNfs($realBuildDir)) { + $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.'); + $fs->remove($realBuildDir); } else { - $fs->remove($realCacheDir); + $fs->rename($realBuildDir, $oldBuildDir); } - $fs->rename($warmupDir, $realCacheDir); + + $fs->rename($warmupDir, $realBuildDir); if ($output->isVerbose()) { - $io->comment('Removing old cache directory...'); + $io->comment('Removing old build and cache directory...'); + } + + if ($useBuildDir) { + try { + $fs->remove($oldBuildDir); + } catch (IOException $e) { + if ($output->isVerbose()) { + $io->warning($e->getMessage()); + } + } } try { @@ -179,7 +200,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function warmup(string $warmupDir, string $realCacheDir, bool $enableOptionalWarmers = true) + private function isNfs(string $dir): bool + { + static $mounts = null; + + if (null === $mounts) { + $mounts = []; + if ('/' === \DIRECTORY_SEPARATOR && $files = @file('/proc/mounts')) { + foreach ($files as $mount) { + $mount = \array_slice(explode(' ', $mount), 1, -3); + if (!\in_array(array_pop($mount), ['vboxsf', 'nfs'])) { + continue; + } + $mounts[] = implode(' ', $mount).'/'; + } + } + } + foreach ($mounts as $mount) { + if (0 === strpos($dir, $mount)) { + return true; + } + } + + return false; + } + + private function warmup(string $warmupDir, string $realBuildDir, bool $enableOptionalWarmers = true) { // create a temporary kernel $kernel = $this->getApplication()->getKernel(); @@ -193,12 +239,16 @@ private function warmup(string $warmupDir, string $realCacheDir, bool $enableOpt $warmer = $kernel->getContainer()->get('cache_warmer'); // non optional warmers already ran during container compilation $warmer->enableOnlyOptionalWarmers(); - $warmer->warmUp($warmupDir); + $preload = (array) $warmer->warmUp($warmupDir); + + if ($preload && file_exists($preloadFile = $warmupDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { + Preloader::append($preloadFile, $preload); + } } // fix references to cached files with the real cache directory name $search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)]; - $replace = str_replace('\\', '/', $realCacheDir); + $replace = str_replace('\\', '/', $realBuildDir); foreach (Finder::create()->files()->in($warmupDir) as $file) { $content = str_replace($search, $replace, file_get_contents($file), $count); if ($count) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php index 123617e58b189..1861645054380 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -25,17 +28,21 @@ * * @author Nicolas Grekas */ +#[AsCommand(name: 'cache:pool:clear', description: 'Clear cache pools')] final class CachePoolClearCommand extends Command { - protected static $defaultName = 'cache:pool:clear'; - private $poolClearer; + private ?array $poolNames; - public function __construct(Psr6CacheClearer $poolClearer) + /** + * @param string[]|null $poolNames + */ + public function __construct(Psr6CacheClearer $poolClearer, array $poolNames = null) { parent::__construct(); $this->poolClearer = $poolClearer; + $this->poolNames = $poolNames; } /** @@ -47,7 +54,6 @@ protected function configure() ->setDefinition([ new InputArgument('pools', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'A list of cache pools or cache pool clearers'), ]) - ->setDescription('Clear cache pools') ->setHelp(<<<'EOF' The %command.name% command clears the given cache pools or cache pool clearers. @@ -88,18 +94,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int $clearer->clear($kernel->getContainer()->getParameter('kernel.cache_dir')); } + $failure = false; foreach ($pools as $id => $pool) { $io->comment(sprintf('Clearing cache pool: %s', $id)); if ($pool instanceof CacheItemPoolInterface) { - $pool->clear(); + if (!$pool->clear()) { + $io->warning(sprintf('Cache pool "%s" could not be cleared.', $pool)); + $failure = true; + } } else { - $this->poolClearer->clearPool($id); + if (false === $this->poolClearer->clearPool($id)) { + $io->warning(sprintf('Cache pool "%s" could not be cleared.', $pool)); + $failure = true; + } } } + if ($failure) { + return 1; + } + $io->success('Cache was successfully cleared.'); return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (\is_array($this->poolNames) && $input->mustSuggestArgumentValuesFor('pools')) { + $suggestions->suggestValues($this->poolNames); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php index 922ec2dd7e94b..16aa16c0004ce 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php @@ -11,7 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -23,17 +26,21 @@ * * @author Pierre du Plessis */ +#[AsCommand(name: 'cache:pool:delete', description: 'Delete an item from a cache pool')] final class CachePoolDeleteCommand extends Command { - protected static $defaultName = 'cache:pool:delete'; - private $poolClearer; + private ?array $poolNames; - public function __construct(Psr6CacheClearer $poolClearer) + /** + * @param string[]|null $poolNames + */ + public function __construct(Psr6CacheClearer $poolClearer, array $poolNames = null) { parent::__construct(); $this->poolClearer = $poolClearer; + $this->poolNames = $poolNames; } /** @@ -46,7 +53,6 @@ protected function configure() new InputArgument('pool', InputArgument::REQUIRED, 'The cache pool from which to delete an item'), new InputArgument('key', InputArgument::REQUIRED, 'The cache key to delete from the pool'), ]) - ->setDescription('Delete an item from a cache pool') ->setHelp(<<<'EOF' The %command.name% deletes an item from a given cache pool. @@ -80,4 +86,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (\is_array($this->poolNames) && $input->mustSuggestArgumentValuesFor('pool')) { + $suggestions->suggestValues($this->poolNames); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php index 7b725411d5015..09ec8b1ef0cc7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -21,12 +22,14 @@ * * @author Tobias Nyholm */ +#[AsCommand(name: 'cache:pool:list', description: 'List available cache pools')] final class CachePoolListCommand extends Command { - protected static $defaultName = 'cache:pool:list'; - - private $poolNames; + private array $poolNames; + /** + * @param string[] $poolNames + */ public function __construct(array $poolNames) { parent::__construct(); @@ -40,7 +43,6 @@ public function __construct(array $poolNames) protected function configure() { $this - ->setDescription('List available cache pools') ->setHelp(<<<'EOF' The %command.name% command lists all available cache pools. EOF diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php index fb9af73064cb4..1e8bb7f0338a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -22,14 +23,13 @@ * * @author Rob Frawley 2nd */ +#[AsCommand(name: 'cache:pool:prune', description: 'Prune cache pools')] final class CachePoolPruneCommand extends Command { - protected static $defaultName = 'cache:pool:prune'; - - private $pools; + private iterable $pools; /** - * @param iterable|PruneableInterface[] $pools + * @param iterable $pools */ public function __construct(iterable $pools) { @@ -44,7 +44,6 @@ public function __construct(iterable $pools) protected function configure() { $this - ->setDescription('Prune cache pools') ->setHelp(<<<'EOF' The %command.name% command deletes all expired items from all pruneable pools. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php index 33a214ea01aa5..7a8e0cc555246 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php @@ -11,11 +11,13 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Dumper\Preloader; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate; /** @@ -25,10 +27,9 @@ * * @final */ +#[AsCommand(name: 'cache:warmup', description: 'Warm up an empty cache')] class CacheWarmupCommand extends Command { - protected static $defaultName = 'cache:warmup'; - private $cacheWarmer; public function __construct(CacheWarmerAggregate $cacheWarmer) @@ -47,7 +48,6 @@ protected function configure() ->setDefinition([ new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'), ]) - ->setDescription('Warm up an empty cache') ->setHelp(<<<'EOF' The %command.name% command warms up the cache. @@ -77,7 +77,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->cacheWarmer->enableOptionalWarmers(); } - $this->cacheWarmer->warmUp($kernel->getContainer()->getParameter('kernel.cache_dir')); + $preload = $this->cacheWarmer->warmUp($cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir')); + + if ($preload && file_exists($preloadFile = $cacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { + Preloader::append($preloadFile, $preload); + } $io->success(sprintf('Cache for the "%s" environment (debug=%s) was successfully warmed.', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php index a0623f396127b..fd01616394763 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php @@ -11,7 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -30,10 +34,9 @@ * * @final */ +#[AsCommand(name: 'debug:config', description: 'Dump the current configuration for an extension')] class ConfigDebugCommand extends AbstractConfigCommand { - protected static $defaultName = 'debug:config'; - /** * {@inheritdoc} */ @@ -44,7 +47,6 @@ protected function configure() new InputArgument('name', InputArgument::OPTIONAL, 'The bundle name or the extension alias'), new InputArgument('path', InputArgument::OPTIONAL, 'The configuration option path'), ]) - ->setDescription('Dump the current configuration for an extension') ->setHelp(<<<'EOF' The %command.name% command dumps the current configuration for an extension/bundle. @@ -73,6 +75,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (null === $name = $input->getArgument('name')) { $this->listBundles($errorIo); + + $kernel = $this->getApplication()->getKernel(); + if ($kernel instanceof ExtensionInterface + && ($kernel instanceof ConfigurationInterface || $kernel instanceof ConfigurationExtensionInterface) + && $kernel->getAlias() + ) { + $errorIo->table(['Kernel Extension'], [[$kernel->getAlias()]]); + } + $errorIo->comment('Provide the name of a bundle as the first argument of this command to dump its configuration. (e.g. debug:config FrameworkBundle)'); $errorIo->comment('For dumping a specific option, add its path as the second argument of this command. (e.g. debug:config FrameworkBundle serializer to dump the framework.serializer configuration)'); @@ -83,11 +94,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extensionAlias = $extension->getAlias(); $container = $this->compileContainer(); - $config = $container->resolveEnvPlaceholders( - $container->getParameterBag()->resolveValue( - $this->getConfigForExtension($extension, $container) - ) - ); + $config = $this->getConfig($extension, $container); if (null === $path = $input->getArgument('path')) { $io->title( @@ -131,10 +138,8 @@ private function compileContainer(): ContainerBuilder * Iterate over configuration until the last step of the given path. * * @throws LogicException If the configuration does not exist - * - * @return mixed */ - private function getConfigForPath(array $config, string $path, string $alias) + private function getConfigForPath(array $config, string $path, string $alias): mixed { $steps = explode('.', $path); @@ -177,4 +182,55 @@ private function getConfigForExtension(ExtensionInterface $extension, ContainerB return (new Processor())->processConfiguration($configuration, $configs); } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->getAvailableBundles(!preg_match('/^[A-Z]/', $input->getCompletionValue()))); + + return; + } + + if ($input->mustSuggestArgumentValuesFor('path') && null !== $name = $input->getArgument('name')) { + try { + $config = $this->getConfig($this->findExtension($name), $this->compileContainer()); + $paths = array_keys(self::buildPathsCompletion($config)); + $suggestions->suggestValues($paths); + } catch (LogicException $e) { + } + } + } + + private function getAvailableBundles(bool $alias): array + { + $availableBundles = []; + foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) { + $availableBundles[] = $alias ? $bundle->getContainerExtension()->getAlias() : $bundle->getName(); + } + + return $availableBundles; + } + + private function getConfig(ExtensionInterface $extension, ContainerBuilder $container) + { + return $container->resolveEnvPlaceholders( + $container->getParameterBag()->resolveValue( + $this->getConfigForExtension($extension, $container) + ) + ); + } + + private static function buildPathsCompletion(array $paths, string $prefix = ''): array + { + $completionPaths = []; + foreach ($paths as $key => $values) { + if (\is_array($values)) { + $completionPaths = $completionPaths + self::buildPathsCompletion($values, $prefix.$key.'.'); + } else { + $completionPaths[$prefix.$key] = null; + } + } + + return $completionPaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index 17690f7c99401..271ba9bf6429b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -11,14 +11,21 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Dumper\XmlReferenceDumper; use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\Yaml\Yaml; /** * A console command for dumping available configuration reference. @@ -29,10 +36,9 @@ * * @final */ +#[AsCommand(name: 'config:dump-reference', description: 'Dump the default configuration for an extension')] class ConfigDumpReferenceCommand extends AbstractConfigCommand { - protected static $defaultName = 'config:dump-reference'; - /** * {@inheritdoc} */ @@ -44,7 +50,6 @@ protected function configure() new InputArgument('path', InputArgument::OPTIONAL, 'The configuration option path'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (yaml or xml)', 'yaml'), ]) - ->setDescription('Dump the default configuration for an extension') ->setHelp(<<<'EOF' The %command.name% command dumps the default configuration for an extension/bundle. @@ -81,6 +86,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (null === $name = $input->getArgument('name')) { $this->listBundles($errorIo); + + $kernel = $this->getApplication()->getKernel(); + if ($kernel instanceof ExtensionInterface + && ($kernel instanceof ConfigurationInterface || $kernel instanceof ConfigurationExtensionInterface) + && $kernel->getAlias() + ) { + $errorIo->table(['Kernel Extension'], [[$kernel->getAlias()]]); + } + $errorIo->comment([ 'Provide the name of a bundle as the first argument of this command to dump its default configuration. (e.g. config:dump-reference FrameworkBundle)', 'For dumping a specific option, add its path as the second argument of this command. (e.g. config:dump-reference FrameworkBundle profiler.matcher to dump the framework.profiler.matcher configuration)', @@ -91,11 +105,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extension = $this->findExtension($name); - $configuration = $extension->getConfiguration([], $this->getContainerBuilder()); + if ($extension instanceof ConfigurationInterface) { + $configuration = $extension; + } else { + $configuration = $extension->getConfiguration([], $this->getContainerBuilder($this->getApplication()->getKernel())); + } $this->validateConfiguration($extension, $configuration); $format = $input->getOption('format'); + + if ('yaml' === $format && !class_exists(Yaml::class)) { + $errorIo->error('Setting the "format" option to "yaml" requires the Symfony Yaml component. Try running "composer install symfony/yaml" or use "--format=xml" instead.'); + + return 1; + } + $path = $input->getArgument('path'); if (null !== $path && 'yaml' !== $format) { @@ -132,4 +157,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->getAvailableBundles()); + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues($this->getAvailableFormatOptions()); + } + } + + private function getAvailableBundles(): array + { + $bundles = []; + + foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) { + $bundles[] = $bundle->getName(); + $bundles[] = $bundle->getContainerExtension()->getAlias(); + } + + return $bundles; + } + + private function getAvailableFormatOptions(): array + { + return ['yaml', 'xml']; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php deleted file mode 100644 index ae7e928b1ff79..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Command; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" with dependency injection instead.', ContainerAwareCommand::class, Command::class), \E_USER_DEPRECATED); - -/** - * Command. - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use {@see Command} instead. - */ -abstract class ContainerAwareCommand extends Command implements ContainerAwareInterface -{ - /** - * @var ContainerInterface|null - */ - private $container; - - /** - * @return ContainerInterface - * - * @throws \LogicException - */ - protected function getContainer() - { - if (null === $this->container) { - $application = $this->getApplication(); - if (null === $application) { - throw new \LogicException('The container cannot be retrieved as the application instance is not yet set.'); - } - - $this->container = $application->getKernel()->getContainer(); - } - - return $this->container; - } - - /** - * {@inheritdoc} - */ - public function setContainer(ContainerInterface $container = null) - { - $this->container = $container; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php index c75ba398a67a4..b579998231215 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -12,19 +12,18 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; -use Symfony\Component\Config\ConfigCache; -use Symfony\Component\Config\FileLocator; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; /** @@ -34,14 +33,10 @@ * * @internal */ +#[AsCommand(name: 'debug:container', description: 'Display current services for an application')] class ContainerDebugCommand extends Command { - protected static $defaultName = 'debug:container'; - - /** - * @var ContainerBuilder|null - */ - protected $containerBuilder; + use BuildDebugContainerTrait; /** * {@inheritdoc} @@ -51,7 +46,6 @@ protected function configure() $this ->setDefinition([ new InputArgument('name', InputArgument::OPTIONAL, 'A service name (foo)'), - new InputOption('show-private', null, InputOption::VALUE_NONE, 'Show public *and* private services (deprecated)'), new InputOption('show-arguments', null, InputOption::VALUE_NONE, 'Show arguments in services'), new InputOption('show-hidden', null, InputOption::VALUE_NONE, 'Show hidden (internal) services'), new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Show all services with a specific tag'), @@ -63,13 +57,17 @@ protected function configure() new InputOption('env-vars', null, InputOption::VALUE_NONE, 'Display environment variables used in the container'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), + new InputOption('deprecations', null, InputOption::VALUE_NONE, 'Display deprecations generated when compiling and warming up the container'), ]) - ->setDescription('Display current services for an application') ->setHelp(<<<'EOF' The %command.name% command displays all configured public services: php %command.full_name% +To see deprecations generated during container compilation and cache warmup, use the --deprecations option: + + php %command.full_name% --deprecations + To get specific information about a service, specify its name: php %command.full_name% validator @@ -121,15 +119,12 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { - if ($input->getOption('show-private')) { - @trigger_error('The "--show-private" option no longer has any effect and is deprecated since Symfony 4.1.', \E_USER_DEPRECATED); - } - $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); $this->validateInput($input); - $object = $this->getContainerBuilder(); + $kernel = $this->getApplication()->getKernel(); + $object = $this->getContainerBuilder($kernel); if ($input->getOption('env-vars')) { $options = ['env-vars' => true]; @@ -154,6 +149,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ($name = $input->getArgument('name')) { $name = $this->findProperServiceName($input, $errorIo, $object, $name, $input->getOption('show-hidden')); $options = ['id' => $name]; + } elseif ($input->getOption('deprecations')) { + $options = ['deprecations' => true]; } else { $options = []; } @@ -164,12 +161,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $options['show_hidden'] = $input->getOption('show-hidden'); $options['raw_text'] = $input->getOption('raw'); $options['output'] = $io; - $options['is_debug'] = $this->getApplication()->getKernel()->isDebug(); + $options['is_debug'] = $kernel->isDebug(); try { $helper->describe($io, $object, $options); - if (isset($options['id']) && isset($this->getApplication()->getKernel()->getContainer()->getRemovedIds()[$options['id']])) { + if (isset($options['id']) && isset($kernel->getContainer()->getRemovedIds()[$options['id']])) { $errorIo->note(sprintf('The "%s" service or alias has been removed or inlined when the container was compiled.', $options['id'])); } } catch (ServiceNotFoundException $e) { @@ -185,7 +182,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorIo->comment('To search for a specific tag, re-run this command with a search term. (e.g. debug:container --tag=form.type)'); } elseif ($input->getOption('parameters')) { $errorIo->comment('To search for a specific parameter, re-run this command with a search term. (e.g. debug:container --parameter=kernel.debug)'); - } else { + } elseif (!$input->getOption('deprecations')) { $errorIo->comment('To search for a specific service, re-run this command with a search term. (e.g. debug:container log)'); } } @@ -193,6 +190,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + + return; + } + + $kernel = $this->getApplication()->getKernel(); + $object = $this->getContainerBuilder($kernel); + + if ($input->mustSuggestArgumentValuesFor('name') + && !$input->getOption('tag') && !$input->getOption('tags') + && !$input->getOption('parameter') && !$input->getOption('parameters') + && !$input->getOption('env-var') && !$input->getOption('env-vars') + && !$input->getOption('types') && !$input->getOption('deprecations') + ) { + $suggestions->suggestValues($this->findServiceIdsContaining( + $object, + $input->getCompletionValue(), + (bool) $input->getOption('show-hidden') + )); + + return; + } + + if ($input->mustSuggestOptionValuesFor('tag')) { + $suggestions->suggestValues($object->findTags()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('parameter')) { + $suggestions->suggestValues(array_keys($object->getParameterBag()->all())); + } + } + /** * Validates input arguments and options. * @@ -211,40 +246,12 @@ protected function validateInput(InputInterface $input) $name = $input->getArgument('name'); if ((null !== $name) && ($optionsCount > 0)) { - throw new InvalidArgumentException('The options tags, tag, parameters & parameter can not be combined with the service name argument.'); + throw new InvalidArgumentException('The options tags, tag, parameters & parameter cannot be combined with the service name argument.'); } elseif ((null === $name) && $optionsCount > 1) { - throw new InvalidArgumentException('The options tags, tag, parameters & parameter can not be combined together.'); + throw new InvalidArgumentException('The options tags, tag, parameters & parameter cannot be combined together.'); } } - /** - * Loads the ContainerBuilder from the cache. - * - * @throws \LogicException - */ - protected function getContainerBuilder(): ContainerBuilder - { - if ($this->containerBuilder) { - return $this->containerBuilder; - } - - $kernel = $this->getApplication()->getKernel(); - - if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { - $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); - $container = $buildContainer(); - $container->getCompilerPassConfig()->setRemovingPasses([]); - $container->getCompilerPassConfig()->setAfterRemovingPasses([]); - $container->compile(); - } else { - (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); - $locatorPass = new ServiceLocatorTagPass(); - $locatorPass->process($container); - } - - return $this->containerBuilder = $container; - } - private function findProperServiceName(InputInterface $input, SymfonyStyle $io, ContainerBuilder $builder, string $name, bool $showHidden): string { $name = ltrim($name, '\\'); @@ -276,7 +283,7 @@ private function findServiceIdsContaining(ContainerBuilder $builder, string $nam if (false !== stripos(str_replace('\\', '', $serviceId), $name)) { $foundServiceIdsIgnoringBackslashes[] = $serviceId; } - if (false !== stripos($serviceId, $name)) { + if ('' === $name || false !== stripos($serviceId, $name)) { $foundServiceIds[] = $serviceId; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index 8225825ae360d..cfd965db98e55 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -13,6 +13,7 @@ use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputInterface; @@ -22,17 +23,14 @@ use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\HttpKernel\Kernel; +#[AsCommand(name: 'lint:container', description: 'Ensure that arguments injected into services match type declarations')] final class ContainerLintCommand extends Command { - protected static $defaultName = 'lint:container'; - - /** - * @var ContainerBuilder - */ private $containerBuilder; /** @@ -41,7 +39,6 @@ final class ContainerLintCommand extends Command protected function configure() { $this - ->setDescription('Ensure that arguments injected into services match type declarations') ->setHelp('This command parses service definitions and ensures that injected values match the type declarations of each services\' class.') ; } @@ -64,14 +61,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int $container->setParameter('container.build_time', time()); - $container->compile(); + try { + $container->compile(); + } catch (InvalidArgumentException $e) { + $errorIo->error($e->getMessage()); + + return 1; + } + + $io->success('The container was linted successfully: all services are injected with values that are compatible with their type declarations.'); return 0; } private function getContainerBuilder(): ContainerBuilder { - if ($this->containerBuilder) { + if (isset($this->containerBuilder)) { return $this->containerBuilder; } @@ -80,7 +85,7 @@ private function getContainerBuilder(): ContainerBuilder if (!$kernel->isDebug() || !(new ConfigCache($kernelContainer->getParameter('debug.container.dump'), true))->isFresh()) { if (!$kernel instanceof Kernel) { - throw new RuntimeException(sprintf('This command does not support the application kernel: "%s" does not extend "%s".', \get_class($kernel), Kernel::class)); + throw new RuntimeException(sprintf('This command does not support the application kernel: "%s" does not extend "%s".', get_debug_type($kernel), Kernel::class)); } $buildContainer = \Closure::bind(function (): ContainerBuilder { @@ -93,7 +98,7 @@ private function getContainerBuilder(): ContainerBuilder $skippedIds = []; } else { if (!$kernelContainer instanceof Container) { - throw new RuntimeException(sprintf('This command does not support the application container: "%s" does not extend "%s".', \get_class($kernelContainer), Container::class)); + throw new RuntimeException(sprintf('This command does not support the application container: "%s" does not extend "%s".', get_debug_type($kernelContainer), Container::class)); } (new XmlFileLoader($container = new ContainerBuilder($parameterBag = new EnvPlaceholderParameterBag()), new FileLocator()))->load($kernelContainer->getParameter('debug.container.dump')); @@ -108,6 +113,10 @@ private function getContainerBuilder(): ContainerBuilder $skippedIds[$serviceId] = true; } } + + $container->getCompilerPassConfig()->setBeforeOptimizationPasses([]); + $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setBeforeRemovingPasses([]); } $container->setParameter('container.build_hash', 'lint_container'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index b5023749a3078..92a76bdcf77e4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -12,6 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Console\Descriptor\Descriptor; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -27,10 +30,10 @@ * * @internal */ +#[AsCommand(name: 'debug:autowiring', description: 'List classes/interfaces you can use for autowiring')] class DebugAutowiringCommand extends ContainerDebugCommand { - protected static $defaultName = 'debug:autowiring'; - private $supportsHref; + private bool $supportsHref; private $fileLinkFormatter; public function __construct(string $name = null, FileLinkFormatter $fileLinkFormatter = null) @@ -50,7 +53,6 @@ protected function configure() new InputArgument('search', InputArgument::OPTIONAL, 'A search filter'), new InputOption('all', null, InputOption::VALUE_NONE, 'Show also services that are not aliased'), ]) - ->setDescription('List classes/interfaces you can use for autowiring') ->setHelp(<<<'EOF' The %command.name% command displays the classes and interfaces that you can use as type-hints for autowiring: @@ -74,13 +76,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); - $builder = $this->getContainerBuilder(); + $builder = $this->getContainerBuilder($this->getApplication()->getKernel()); $serviceIds = $builder->getServiceIds(); $serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']); if ($search = $input->getArgument('search')) { - $serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) { - return false !== stripos(str_replace('\\', '', $serviceId), $search) && !str_starts_with($serviceId, '.'); + $searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff $]++/', '', $search); + + $serviceIds = array_filter($serviceIds, function ($serviceId) use ($searchNormalized) { + return false !== stripos(str_replace('\\', '', $serviceId), $searchNormalized) && !str_starts_with($serviceId, '.'); }); if (empty($serviceIds)) { @@ -153,10 +157,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getFileLink(string $class): string { if (null === $this->fileLinkFormatter - || (null === $r = $this->getContainerBuilder()->getReflectionClass($class, false))) { + || (null === $r = $this->getContainerBuilder($this->getApplication()->getKernel())->getReflectionClass($class, false))) { return ''; } return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine()); } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('search')) { + $builder = $this->getContainerBuilder($this->getApplication()->getKernel()); + + $suggestions->suggestValues(array_filter($builder->getServiceIds(), [$this, 'filterToServiceTypes'])); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php index fd0a1ccb800e7..0a8cdaf04aac7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php @@ -11,14 +11,19 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ServiceProviderInterface; /** * A console command for retrieving information about event dispatcher. @@ -27,16 +32,18 @@ * * @final */ +#[AsCommand(name: 'debug:event-dispatcher', description: 'Display configured listeners for an application')] class EventDispatcherDebugCommand extends Command { - protected static $defaultName = 'debug:event-dispatcher'; - private $dispatcher; + private const DEFAULT_DISPATCHER = 'event_dispatcher'; - public function __construct(EventDispatcherInterface $dispatcher) + private $dispatchers; + + public function __construct(ContainerInterface $dispatchers) { parent::__construct(); - $this->dispatcher = $dispatcher; + $this->dispatchers = $dispatchers; } /** @@ -46,11 +53,11 @@ protected function configure() { $this ->setDefinition([ - new InputArgument('event', InputArgument::OPTIONAL, 'An event name'), + new InputArgument('event', InputArgument::OPTIONAL, 'An event name or a part of the event name'), + new InputOption('dispatcher', null, InputOption::VALUE_REQUIRED, 'To view events of a specific event dispatcher', self::DEFAULT_DISPATCHER), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), ]) - ->setDescription('Display configured listeners for an application') ->setHelp(<<<'EOF' The %command.name% command displays all configured listeners: @@ -74,22 +81,83 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $options = []; - if ($event = $input->getArgument('event')) { - if (!$this->dispatcher->hasListeners($event)) { - $io->getErrorStyle()->warning(sprintf('The event "%s" does not have any registered listeners.', $event)); + $dispatcherServiceName = $input->getOption('dispatcher'); + if (!$this->dispatchers->has($dispatcherServiceName)) { + $io->getErrorStyle()->error(sprintf('Event dispatcher "%s" is not available.', $dispatcherServiceName)); - return 0; - } + return 1; + } - $options = ['event' => $event]; + $dispatcher = $this->dispatchers->get($dispatcherServiceName); + + if ($event = $input->getArgument('event')) { + if ($dispatcher->hasListeners($event)) { + $options = ['event' => $event]; + } else { + // if there is no direct match, try find partial matches + $events = $this->searchForEvent($dispatcher, $event); + if (0 === \count($events)) { + $io->getErrorStyle()->warning(sprintf('The event "%s" does not have any registered listeners.', $event)); + + return 0; + } elseif (1 === \count($events)) { + $options = ['event' => $events[array_key_first($events)]]; + } else { + $options = ['events' => $events]; + } + } } $helper = new DescriptorHelper(); + + if (self::DEFAULT_DISPATCHER !== $dispatcherServiceName) { + $options['dispatcher_service_name'] = $dispatcherServiceName; + } + $options['format'] = $input->getOption('format'); $options['raw_text'] = $input->getOption('raw'); $options['output'] = $io; - $helper->describe($io, $this->dispatcher, $options); + $helper->describe($io, $dispatcher, $options); return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('event')) { + $dispatcherServiceName = $input->getOption('dispatcher'); + if ($this->dispatchers->has($dispatcherServiceName)) { + $dispatcher = $this->dispatchers->get($dispatcherServiceName); + $suggestions->suggestValues(array_keys($dispatcher->getListeners())); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('dispatcher')) { + if ($this->dispatchers instanceof ServiceProviderInterface) { + $suggestions->suggestValues(array_keys($this->dispatchers->getProvidedServices())); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues((new DescriptorHelper())->getFormats()); + } + } + + private function searchForEvent(EventDispatcherInterface $dispatcher, string $needle): array + { + $output = []; + $lcNeedle = strtolower($needle); + $allEvents = array_keys($dispatcher->getListeners()); + foreach ($allEvents as $event) { + if (str_contains(strtolower($event), $lcNeedle)) { + $output[] = $event; + } + } + + return $output; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 22cac5256bbc2..cf2c93b05e781 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -31,9 +34,11 @@ * * @final */ +#[AsCommand(name: 'debug:router', description: 'Display current routes for an application')] class RouterDebugCommand extends Command { - protected static $defaultName = 'debug:router'; + use BuildDebugContainerTrait; + private $router; private $fileLinkFormatter; @@ -57,7 +62,6 @@ protected function configure() new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), ]) - ->setDescription('Display current routes for an application') ->setHelp(<<<'EOF' The %command.name% displays the configured routes: @@ -79,6 +83,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $name = $input->getArgument('name'); $helper = new DescriptorHelper($this->fileLinkFormatter); $routes = $this->router->getRouteCollection(); + $container = null; + if ($this->fileLinkFormatter) { + $container = function () { + return $this->getContainerBuilder($this->getApplication()->getKernel()); + }; + } if ($name) { if (!($route = $routes->get($name)) && $matchingRoutes = $this->findRouteNameContaining($name, $routes)) { @@ -96,6 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'raw_text' => $input->getOption('raw'), 'name' => $name, 'output' => $io, + 'container' => $container, ]); } else { $helper->describe($io, $routes, [ @@ -103,6 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'raw_text' => $input->getOption('raw'), 'show_controllers' => $input->getOption('show-controllers'), 'output' => $io, + 'container' => $container, ]); } @@ -120,4 +132,18 @@ private function findRouteNameContaining(string $name, RouteCollection $routes): return $foundRoutesNames; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->router->getRouteCollection()->all())); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php index 8dd5b545b40c9..d61b3c1345adb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; @@ -18,6 +19,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\Routing\Matcher\TraceableUrlMatcher; use Symfony\Component\Routing\RouterInterface; @@ -28,17 +30,21 @@ * * @final */ +#[AsCommand(name: 'router:match', description: 'Help debug routes by simulating a path info match')] class RouterMatchCommand extends Command { - protected static $defaultName = 'router:match'; - private $router; + private iterable $expressionLanguageProviders; - public function __construct(RouterInterface $router) + /** + * @param iterable $expressionLanguageProviders + */ + public function __construct(RouterInterface $router, iterable $expressionLanguageProviders = []) { parent::__construct(); $this->router = $router; + $this->expressionLanguageProviders = $expressionLanguageProviders; } /** @@ -53,7 +59,6 @@ protected function configure() new InputOption('scheme', null, InputOption::VALUE_REQUIRED, 'Set the URI scheme (usually http or https)'), new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Set the URI host'), ]) - ->setDescription('Help debug routes by simulating a path info match') ->setHelp(<<<'EOF' The %command.name% shows which routes match a given request and which don't and for what reason: @@ -87,6 +92,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $matcher = new TraceableUrlMatcher($this->router->getRouteCollection(), $context); + foreach ($this->expressionLanguageProviders as $provider) { + $matcher->addExpressionLanguageProvider($provider); + } $traces = $matcher->getTraces($input->getArgument('path_info')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php index 6d8820443a2c1..255de463ef6df 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -24,10 +25,9 @@ * * @internal */ +#[AsCommand(name: 'secrets:decrypt-to-local', description: 'Decrypt all secrets and stores them in the local vault')] final class SecretsDecryptToLocalCommand extends Command { - protected static $defaultName = 'secrets:decrypt-to-local'; - private $vault; private $localVault; @@ -42,7 +42,6 @@ public function __construct(AbstractVault $vault, AbstractVault $localVault = nu protected function configure() { $this - ->setDescription('Decrypt all secrets and stores them in the local vault.') ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force overriding of secrets that already exist in the local vault') ->setHelp(<<<'EOF' The %command.name% command decrypts all secrets and copies them in the local vault. @@ -69,12 +68,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int $secrets = $this->vault->list(true); + $io->comment(sprintf('%d secret%s found in the vault.', \count($secrets), 1 !== \count($secrets) ? 's' : '')); + + $skipped = 0; if (!$input->getOption('force')) { foreach ($this->localVault->list() as $k => $v) { - unset($secrets[$k]); + if (isset($secrets[$k])) { + ++$skipped; + unset($secrets[$k]); + } } } + if ($skipped > 0) { + $io->warning([ + sprintf('%d secret%s already overridden in the local vault and will be skipped.', $skipped, 1 !== $skipped ? 's are' : ' is'), + 'Use the --force flag to override these.', + ]); + } + foreach ($secrets as $k => $v) { if (null === $v) { $io->error($this->vault->getLastMessage() ?? sprintf('Secret "%s" has been skipped as there was an error reading it.', $k)); @@ -82,6 +94,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->localVault->seal($k, $v); + $io->note($this->localVault->getLastMessage()); } return 0; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php index 14e7c51e6df53..439e060a2d5bb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -23,10 +24,9 @@ * * @internal */ +#[AsCommand(name: 'secrets:encrypt-from-local', description: 'Encrypt all local secrets to the vault')] final class SecretsEncryptFromLocalCommand extends Command { - protected static $defaultName = 'secrets:encrypt-from-local'; - private $vault; private $localVault; @@ -41,7 +41,6 @@ public function __construct(AbstractVault $vault, AbstractVault $localVault = nu protected function configure() { $this - ->setDescription('Encrypt all local secrets to the vault.') ->setHelp(<<<'EOF' The %command.name% command encrypts all locally overridden secrets to the vault. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php index f0497815627cd..eeeaa2a391db6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,10 +27,9 @@ * * @internal */ +#[AsCommand(name: 'secrets:generate-keys', description: 'Generate new encryption keys')] final class SecretsGenerateKeysCommand extends Command { - protected static $defaultName = 'secrets:generate-keys'; - private $vault; private $localVault; @@ -44,7 +44,6 @@ public function __construct(AbstractVault $vault, AbstractVault $localVault = nu protected function configure() { $this - ->setDescription('Generate new encryption keys.') ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') ->addOption('rotate', 'r', InputOption::VALUE_NONE, 'Re-encrypt existing secrets with the newly generated keys.') ->setHelp(<<<'EOF' diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php index 4586677f785df..1ce6bbcb7dc89 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Input\InputInterface; @@ -27,10 +28,9 @@ * * @internal */ +#[AsCommand(name: 'secrets:list', description: 'List all secrets')] final class SecretsListCommand extends Command { - protected static $defaultName = 'secrets:list'; - private $vault; private $localVault; @@ -45,7 +45,6 @@ public function __construct(AbstractVault $vault, AbstractVault $localVault = nu protected function configure() { $this - ->setDescription('List all secrets.') ->addOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names') ->setHelp(<<<'EOF' The %command.name% command list all stored secrets. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php index f4c40a8fdec8c..5e68295e715e4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -26,10 +29,9 @@ * * @internal */ +#[AsCommand(name: 'secrets:remove', description: 'Remove a secret from the vault')] final class SecretsRemoveCommand extends Command { - protected static $defaultName = 'secrets:remove'; - private $vault; private $localVault; @@ -44,7 +46,6 @@ public function __construct(AbstractVault $vault, AbstractVault $localVault = nu protected function configure() { $this - ->setDescription('Remove a secret from the vault.') ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret') ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') ->setHelp(<<<'EOF' @@ -79,4 +80,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if (!$input->mustSuggestArgumentValuesFor('name')) { + return; + } + + $vaultKeys = array_keys($this->vault->list(false)); + if ($input->getOption('local')) { + if (null === $this->localVault) { + return; + } + $vaultKeys = array_intersect($vaultKeys, array_keys($this->localVault->list(false))); + } + + $suggestions->suggestValues($vaultKeys); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php index ad7559d3f7ce3..21113b759b23f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -27,10 +30,9 @@ * * @internal */ +#[AsCommand(name: 'secrets:set', description: 'Set a secret in the vault')] final class SecretsSetCommand extends Command { - protected static $defaultName = 'secrets:set'; - private $vault; private $localVault; @@ -45,7 +47,6 @@ public function __construct(AbstractVault $vault, AbstractVault $localVault = nu protected function configure() { $this - ->setDescription('Set a secret in the vault.') ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret') ->addArgument('file', InputArgument::OPTIONAL, 'A file where to read the secret from or "-" for reading from STDIN') ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') @@ -136,4 +137,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->vault->list(false))); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index 984c72e59f795..68735f922dd40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -11,7 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -26,7 +29,6 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Translator; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -37,30 +39,28 @@ * * @final */ +#[AsCommand(name: 'debug:translation', description: 'Display translation messages information')] class TranslationDebugCommand extends Command { + public const EXIT_CODE_GENERAL_ERROR = 64; + public const EXIT_CODE_MISSING = 65; + public const EXIT_CODE_UNUSED = 66; + public const EXIT_CODE_FALLBACK = 68; public const MESSAGE_MISSING = 0; public const MESSAGE_UNUSED = 1; public const MESSAGE_EQUALS_FALLBACK = 2; - protected static $defaultName = 'debug:translation'; - private $translator; private $reader; private $extractor; - private $defaultTransPath; - private $defaultViewsPath; - private $transPaths; - private $viewsPaths; + private ?string $defaultTransPath; + private ?string $defaultViewsPath; + private array $transPaths; + private array $codePaths; + private array $enabledLocales; - /** - * @param TranslatorInterface $translator - */ - public function __construct($translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $viewsPaths = []) + public function __construct(TranslatorInterface $translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) { - if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } parent::__construct(); $this->translator = $translator; @@ -69,7 +69,8 @@ public function __construct($translator, TranslationReaderInterface $reader, Ext $this->defaultTransPath = $defaultTransPath; $this->defaultViewsPath = $defaultViewsPath; $this->transPaths = $transPaths; - $this->viewsPaths = $viewsPaths; + $this->codePaths = $codePaths; + $this->enabledLocales = $enabledLocales; } /** @@ -86,7 +87,6 @@ protected function configure() new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Display only unused messages'), new InputOption('all', null, InputOption::VALUE_NONE, 'Load messages from all registered bundles'), ]) - ->setDescription('Display translation messages information') ->setHelp(<<<'EOF' The %command.name% command helps finding unused or missing translation messages and comparing them with the fallback ones by inspecting the @@ -130,33 +130,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $locale = $input->getArgument('locale'); $domain = $input->getOption('domain'); + + $exitCode = self::SUCCESS; + /** @var KernelInterface $kernel */ $kernel = $this->getApplication()->getKernel(); - $rootDir = $kernel->getContainer()->getParameter('kernel.root_dir'); // Define Root Paths - $transPaths = $this->transPaths; - if (is_dir($dir = $rootDir.'/Resources/translations')) { - if ($dir !== $this->defaultTransPath) { - $notice = sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); - } - $transPaths[] = $dir; - } - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - $viewsPaths = $this->viewsPaths; - if (is_dir($dir = $rootDir.'/Resources/views')) { - if ($dir !== $this->defaultViewsPath) { - $notice = sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); - } - $viewsPaths[] = $dir; - } - if ($this->defaultViewsPath) { - $viewsPaths[] = $this->defaultViewsPath; - } + $transPaths = $this->getRootTransPaths(); + $codePaths = $this->getRootCodePaths($kernel); // Override with provided Bundle info if (null !== $input->getArgument('bundle')) { @@ -164,44 +146,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $bundle = $kernel->getBundle($input->getArgument('bundle')); $bundleDir = $bundle->getPath(); $transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations']; - $viewsPaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; + $codePaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; if ($this->defaultTransPath) { $transPaths[] = $this->defaultTransPath; } - if (is_dir($dir = sprintf('%s/Resources/%s/translations', $rootDir, $bundle->getName()))) { - $transPaths[] = $dir; - $notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $dir, $bundle->getName()); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); - } if ($this->defaultViewsPath) { - $viewsPaths[] = $this->defaultViewsPath; - } - if (is_dir($dir = sprintf('%s/Resources/%s/views', $rootDir, $bundle->getName()))) { - $viewsPaths[] = $dir; - $notice = sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); + $codePaths[] = $this->defaultViewsPath; } } catch (\InvalidArgumentException $e) { // such a bundle does not exist, so treat the argument as path $path = $input->getArgument('bundle'); $transPaths = [$path.'/translations']; - if (is_dir($dir = $path.'/Resources/translations')) { - if ($dir !== $this->defaultTransPath) { - @trigger_error(sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/translations'), \E_USER_DEPRECATED); - } - $transPaths[] = $dir; - } + $codePaths = [$path.'/templates']; - $viewsPaths = [$path.'/templates']; - if (is_dir($dir = $path.'/Resources/views')) { - if ($dir !== $this->defaultViewsPath) { - @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/templates'), \E_USER_DEPRECATED); - } - $viewsPaths[] = $dir; - } - - if (!is_dir($transPaths[0]) && !isset($transPaths[1])) { + if (!is_dir($transPaths[0])) { throw new InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); } } @@ -209,22 +168,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int foreach ($kernel->getBundles() as $bundle) { $bundleDir = $bundle->getPath(); $transPaths[] = is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundle->getPath().'/translations'; - $viewsPaths[] = is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundle->getPath().'/templates'; - if (is_dir($deprecatedPath = sprintf('%s/Resources/%s/translations', $rootDir, $bundle->getName()))) { - $transPaths[] = $deprecatedPath; - $notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $deprecatedPath); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); - } - if (is_dir($deprecatedPath = sprintf('%s/Resources/%s/views', $rootDir, $bundle->getName()))) { - $viewsPaths[] = $deprecatedPath; - $notice = sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $deprecatedPath); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); - } + $codePaths[] = is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundle->getPath().'/templates'; } } // Extract used messages - $extractedCatalogue = $this->extractMessages($locale, $viewsPaths); + $extractedCatalogue = $this->extractMessages($locale, $codePaths); // Load defined messages $currentCatalogue = $this->loadCurrentMessages($locale, $transPaths); @@ -246,7 +195,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->getErrorStyle()->warning($outputMessage); - return 0; + return self::EXIT_CODE_GENERAL_ERROR; } // Load the fallback catalogues @@ -267,13 +216,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($extractedCatalogue->defines($messageId, $domain)) { if (!$currentCatalogue->defines($messageId, $domain)) { $states[] = self::MESSAGE_MISSING; + + if (!$input->getOption('only-unused')) { + $exitCode = $exitCode | self::EXIT_CODE_MISSING; + } } } elseif ($currentCatalogue->defines($messageId, $domain)) { $states[] = self::MESSAGE_UNUSED; + + if (!$input->getOption('only-missing')) { + $exitCode = $exitCode | self::EXIT_CODE_UNUSED; + } } - if (!\in_array(self::MESSAGE_UNUSED, $states) && true === $input->getOption('only-unused') - || !\in_array(self::MESSAGE_MISSING, $states) && true === $input->getOption('only-missing')) { + if (!\in_array(self::MESSAGE_UNUSED, $states) && $input->getOption('only-unused') + || !\in_array(self::MESSAGE_MISSING, $states) && $input->getOption('only-missing') + ) { continue; } @@ -281,6 +239,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($fallbackCatalogue->defines($messageId, $domain) && $value === $fallbackCatalogue->get($messageId, $domain)) { $states[] = self::MESSAGE_EQUALS_FALLBACK; + $exitCode = $exitCode | self::EXIT_CODE_FALLBACK; + break; } } @@ -296,7 +256,45 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->table($headers, $rows); - return 0; + return $exitCode; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('locale')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + + if ($input->mustSuggestArgumentValuesFor('bundle')) { + $availableBundles = []; + foreach ($kernel->getBundles() as $bundle) { + $availableBundles[] = $bundle->getName(); + + if ($extension = $bundle->getContainerExtension()) { + $availableBundles[] = $extension->getAlias(); + } + } + + $suggestions->suggestValues($availableBundles); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domain')) { + $locale = $input->getArgument('locale'); + + $mergeOperation = new MergeOperation( + $this->extractMessages($locale, $this->getRootCodePaths($kernel)), + $this->loadCurrentMessages($locale, $this->getRootTransPaths()) + ); + + $suggestions->suggestValues($mergeOperation->getDomains()); + } } private function formatState(int $state): string @@ -394,4 +392,25 @@ private function loadFallbackCatalogues(string $locale, array $transPaths): arra return $fallbackCatalogues; } + + private function getRootTransPaths(): array + { + $transPaths = $this->transPaths; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + + return $transPaths; + } + + private function getRootCodePaths(KernelInterface $kernel): array + { + $codePaths = $this->codePaths; + $codePaths[] = $kernel->getProjectDir().'/src'; + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + + return $codePaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index 1004ae7899dc6..e3128ef3c5c1c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -11,11 +11,15 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\KernelInterface; @@ -35,24 +39,28 @@ * * @final */ +#[AsCommand(name: 'translation:extract', description: 'Extract missing translations keys from code to translation files.')] class TranslationUpdateCommand extends Command { private const ASC = 'asc'; private const DESC = 'desc'; private const SORT_ORDERS = [self::ASC, self::DESC]; - - protected static $defaultName = 'translation:update'; + private const FORMATS = [ + 'xlf12' => ['xlf', '1.2'], + 'xlf20' => ['xlf', '2.0'], + ]; private $writer; private $reader; private $extractor; - private $defaultLocale; - private $defaultTransPath; - private $defaultViewsPath; - private $transPaths; - private $viewsPaths; - - public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $viewsPaths = []) + private string $defaultLocale; + private ?string $defaultTransPath; + private ?string $defaultViewsPath; + private array $transPaths; + private array $codePaths; + private array $enabledLocales; + + public function __construct(TranslationWriterInterface $writer, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultLocale, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $codePaths = [], array $enabledLocales = []) { parent::__construct(); @@ -63,7 +71,8 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade $this->defaultTransPath = $defaultTransPath; $this->defaultViewsPath = $defaultViewsPath; $this->transPaths = $transPaths; - $this->viewsPaths = $viewsPaths; + $this->codePaths = $codePaths; + $this->enabledLocales = $enabledLocales; } /** @@ -76,16 +85,14 @@ protected function configure() new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'), new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'), - new InputOption('output-format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'), new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), - new InputOption('force', null, InputOption::VALUE_NONE, 'Should the update be done'), - new InputOption('no-backup', null, InputOption::VALUE_NONE, 'Should backup be disabled'), + new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), - new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to update'), - new InputOption('xliff-version', null, InputOption::VALUE_OPTIONAL, 'Override the default xliff version', '1.2'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically', 'asc'), + new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), ]) - ->setDescription('Update the translation file') ->setHelp(<<<'EOF' The %command.name% command extracts translation strings from templates of a given bundle or the default translations directory. It can display them or merge @@ -108,6 +115,12 @@ protected function configure() php %command.full_name% --dump-messages --sort=asc en AcmeBundle php %command.full_name% --dump-messages --sort=desc fr + +You can dump a tree-like structure using the yaml format with --as-tree flag: + + php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle + php %command.full_name% --force --format=yaml --sort=asc --as-tree=3 fr + EOF ) ; @@ -118,6 +131,13 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { + $io = new SymfonyStyle($input, $output); + $errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io; + + if ('translation:update' === $input->getFirstArgument()) { + $errorIo->caution('Command "translation:update" is deprecated since version 5.4 and will be removed in Symfony 6.0. Use "translation:extract" instead.'); + } + $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); @@ -128,40 +148,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } + $format = $input->getOption('format'); + $xliffVersion = '1.2'; + + if (\in_array($format, array_keys(self::FORMATS), true)) { + [$format, $xliffVersion] = self::FORMATS[$format]; + } + // check format $supportedFormats = $this->writer->getFormats(); - if (!\in_array($input->getOption('output-format'), $supportedFormats, true)) { - $errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).'.']); + if (!\in_array($format, $supportedFormats, true)) { + $errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).', xlf12 and xlf20.']); return 1; } + /** @var KernelInterface $kernel */ $kernel = $this->getApplication()->getKernel(); - $rootDir = $kernel->getContainer()->getParameter('kernel.root_dir'); // Define Root Paths - $transPaths = $this->transPaths; - if (is_dir($dir = $rootDir.'/Resources/translations')) { - if ($dir !== $this->defaultTransPath) { - $notice = sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); - } - $transPaths[] = $dir; - } - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - $viewsPaths = $this->viewsPaths; - if (is_dir($dir = $rootDir.'/Resources/views')) { - if ($dir !== $this->defaultViewsPath) { - $notice = sprintf('Storing templates in the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); - } - $viewsPaths[] = $dir; - } - if ($this->defaultViewsPath) { - $viewsPaths[] = $this->defaultViewsPath; - } + $transPaths = $this->getRootTransPaths(); + $codePaths = $this->getRootCodePaths($kernel); + $currentName = 'default directory'; // Override with provided Bundle info @@ -170,22 +178,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $foundBundle = $kernel->getBundle($input->getArgument('bundle')); $bundleDir = $foundBundle->getPath(); $transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations']; - $viewsPaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; + $codePaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; if ($this->defaultTransPath) { $transPaths[] = $this->defaultTransPath; } - if (is_dir($dir = sprintf('%s/Resources/%s/translations', $rootDir, $foundBundle->getName()))) { - $transPaths[] = $dir; - $notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $foundBundle->getName(), $dir); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); - } if ($this->defaultViewsPath) { - $viewsPaths[] = $this->defaultViewsPath; - } - if (is_dir($dir = sprintf('%s/Resources/%s/views', $rootDir, $foundBundle->getName()))) { - $viewsPaths[] = $dir; - $notice = sprintf('Storing templates for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $foundBundle->getName(), $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); + $codePaths[] = $this->defaultViewsPath; } $currentName = $foundBundle->getName(); } catch (\InvalidArgumentException $e) { @@ -193,22 +191,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $path = $input->getArgument('bundle'); $transPaths = [$path.'/translations']; - if (is_dir($dir = $path.'/Resources/translations')) { - if ($dir !== $this->defaultTransPath) { - @trigger_error(sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/translations'), \E_USER_DEPRECATED); - } - $transPaths[] = $dir; - } - - $viewsPaths = [$path.'/templates']; - if (is_dir($dir = $path.'/Resources/views')) { - if ($dir !== $this->defaultViewsPath) { - @trigger_error(sprintf('Storing templates in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/templates'), \E_USER_DEPRECATED); - } - $viewsPaths[] = $dir; - } + $codePaths = [$path.'/templates']; - if (!is_dir($transPaths[0]) && !isset($transPaths[1])) { + if (!is_dir($transPaths[0])) { throw new InvalidArgumentException(sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); } } @@ -217,24 +202,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->title('Translation Messages Extractor and Dumper'); $io->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); - // load any messages from templates - $extractedCatalogue = new MessageCatalogue($input->getArgument('locale')); $io->comment('Parsing templates...'); - $this->extractor->setPrefix($input->getOption('prefix')); - foreach ($viewsPaths as $path) { - if (is_dir($path) || is_file($path)) { - $this->extractor->extract($path, $extractedCatalogue); - } - } + $extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $input->getOption('prefix')); - // load any existing messages from the translation files - $currentCatalogue = new MessageCatalogue($input->getArgument('locale')); $io->comment('Loading translation files...'); - foreach ($transPaths as $path) { - if (is_dir($path)) { - $this->reader->read($path, $currentCatalogue); - } - } + $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths); if (null !== $domain = $input->getOption('domain')) { $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); @@ -255,23 +227,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $resultMessage = 'Translation files were successfully updated'; - // move new messages to intl domain when possible - if (class_exists(\MessageFormatter::class)) { - foreach ($operation->getDomains() as $domain) { - $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; - $newMessages = $operation->getNewMessages($domain); - - if ([] === $newMessages || ([] === $currentCatalogue->all($intlDomain) && [] !== $currentCatalogue->all($domain))) { - continue; - } - - $result = $operation->getResult(); - $allIntlMessages = $result->all($intlDomain); - $currentMessages = array_diff_key($newMessages, $result->all($domain)); - $result->replace($currentMessages, $domain); - $result->replace($allIntlMessages + $newMessages, $intlDomain); - } - } + $operation->moveMessagesToIntlDomainsIfPossible('new'); // show compiled list of messages if (true === $input->getOption('dump-messages')) { @@ -314,17 +270,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $extractedMessagesCount += $domainMessagesCount; } - if ('xlf' === $input->getOption('output-format')) { - $io->comment(sprintf('Xliff output version is %s', $input->getOption('xliff-version'))); + if ('xlf' === $format) { + $io->comment(sprintf('Xliff output version is %s', $xliffVersion)); } $resultMessage = sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was'); } - if (true === $input->getOption('no-backup')) { - $this->writer->disableBackup(); - } - // save the files if (true === $input->getOption('force')) { $io->comment('Writing files...'); @@ -340,7 +292,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $bundleTransPath = end($transPaths); } - $this->writer->write($operation->getResult(), $input->getOption('output-format'), ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $input->getOption('xliff-version')]); + $this->writer->write($operation->getResult(), $format, ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $xliffVersion, 'as_tree' => $input->getOption('as-tree'), 'inline' => $input->getOption('as-tree') ?? 0]); if (true === $input->getOption('dump-messages')) { $resultMessage .= ' and translation files were updated'; @@ -352,6 +304,60 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('locale')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + if ($input->mustSuggestArgumentValuesFor('bundle')) { + $bundles = []; + + foreach ($kernel->getBundles() as $bundle) { + $bundles[] = $bundle->getName(); + if ($bundle->getContainerExtension()) { + $bundles[] = $bundle->getContainerExtension()->getAlias(); + } + } + + $suggestions->suggestValues($bundles); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(array_merge( + $this->writer->getFormats(), + array_keys(self::FORMATS) + )); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domain') && $locale = $input->getArgument('locale')) { + $extractedCatalogue = $this->extractMessages($locale, $this->getRootCodePaths($kernel), $input->getOption('prefix')); + + $currentCatalogue = $this->loadCurrentMessages($locale, $this->getRootTransPaths()); + + // process catalogues + $operation = $input->getOption('clean') + ? new TargetOperation($currentCatalogue, $extractedCatalogue) + : new MergeOperation($currentCatalogue, $extractedCatalogue); + + $suggestions->suggestValues($operation->getDomains()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('sort')) { + $suggestions->suggestValues(self::SORT_ORDERS); + } + } + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue { $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); @@ -369,11 +375,13 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M foreach ($catalogue->getResources() as $resource) { $filteredCatalogue->addResource($resource); } + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $intlDomain); } } + if ($metadata = $catalogue->getMetadata('', $domain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $domain); @@ -382,4 +390,50 @@ private function filterCatalogue(MessageCatalogue $catalogue, string $domain): M return $filteredCatalogue; } + + private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue + { + $extractedCatalogue = new MessageCatalogue($locale); + $this->extractor->setPrefix($prefix); + foreach ($transPaths as $path) { + if (is_dir($path) || is_file($path)) { + $this->extractor->extract($path, $extractedCatalogue); + } + } + + return $extractedCatalogue; + } + + private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue + { + $currentCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + if (is_dir($path)) { + $this->reader->read($path, $currentCatalogue); + } + } + + return $currentCatalogue; + } + + private function getRootTransPaths(): array + { + $transPaths = $this->transPaths; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + + return $transPaths; + } + + private function getRootCodePaths(KernelInterface $kernel): array + { + $codePaths = $this->codePaths; + $codePaths[] = $kernel->getProjectDir().'/src'; + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + + return $codePaths; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php index ecdca7cb39452..eb96e65470ebf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php @@ -11,13 +11,18 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Dumper\GraphvizDumper; +use Symfony\Component\Workflow\Dumper\MermaidDumper; use Symfony\Component\Workflow\Dumper\PlantUmlDumper; use Symfony\Component\Workflow\Dumper\StateMachineGraphvizDumper; use Symfony\Component\Workflow\Marking; @@ -27,9 +32,28 @@ * * @final */ +#[AsCommand(name: 'workflow:dump', description: 'Dump a workflow')] class WorkflowDumpCommand extends Command { - protected static $defaultName = 'workflow:dump'; + /** + * string is the service id. + * + * @var array + */ + private array $workflows = []; + + private const DUMP_FORMAT_OPTIONS = [ + 'puml', + 'mermaid', + 'dot', + ]; + + public function __construct(array $workflows) + { + parent::__construct(); + + $this->workflows = $workflows; + } /** * {@inheritdoc} @@ -41,16 +65,15 @@ protected function configure() new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'), new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'), new InputOption('label', 'l', InputOption::VALUE_REQUIRED, 'Label a graph'), - new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format [dot|puml]', 'dot'), + new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format ['.implode('|', self::DUMP_FORMAT_OPTIONS).']', 'dot'), ]) - ->setDescription('Dump a workflow') ->setHelp(<<<'EOF' The %command.name% command dumps the graphical representation of a workflow in different formats DOT: %command.full_name% | dot -Tpng > workflow.png PUML: %command.full_name% --dump-format=puml | java -jar plantuml.jar -p > workflow.png - +MERMAID: %command.full_name% --dump-format=mermaid | mmdc -o workflow.svg EOF ) ; @@ -61,26 +84,36 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { - $container = $this->getApplication()->getKernel()->getContainer(); - $serviceId = $input->getArgument('name'); + $workflowName = $input->getArgument('name'); - if ($container->has('workflow.'.$serviceId)) { - $workflow = $container->get('workflow.'.$serviceId); + $workflow = null; + + if (isset($this->workflows['workflow.'.$workflowName])) { + $workflow = $this->workflows['workflow.'.$workflowName]; $type = 'workflow'; - } elseif ($container->has('state_machine.'.$serviceId)) { - $workflow = $container->get('state_machine.'.$serviceId); + } elseif (isset($this->workflows['state_machine.'.$workflowName])) { + $workflow = $this->workflows['state_machine.'.$workflowName]; $type = 'state_machine'; - } else { - throw new InvalidArgumentException(sprintf('No service found for "workflow.%1$s" nor "state_machine.%1$s".', $serviceId)); } - if ('puml' === $input->getOption('dump-format')) { - $transitionType = 'workflow' === $type ? PlantUmlDumper::WORKFLOW_TRANSITION : PlantUmlDumper::STATEMACHINE_TRANSITION; - $dumper = new PlantUmlDumper($transitionType); - } elseif ('workflow' === $type) { - $dumper = new GraphvizDumper(); - } else { - $dumper = new StateMachineGraphvizDumper(); + if (null === $workflow) { + throw new InvalidArgumentException(sprintf('No service found for "workflow.%1$s" nor "state_machine.%1$s".', $workflowName)); + } + + switch ($input->getOption('dump-format')) { + case 'puml': + $transitionType = 'workflow' === $type ? PlantUmlDumper::WORKFLOW_TRANSITION : PlantUmlDumper::STATEMACHINE_TRANSITION; + $dumper = new PlantUmlDumper($transitionType); + break; + + case 'mermaid': + $transitionType = 'workflow' === $type ? MermaidDumper::TRANSITION_TYPE_WORKFLOW : MermaidDumper::TRANSITION_TYPE_STATEMACHINE; + $dumper = new MermaidDumper($transitionType); + break; + + case 'dot': + default: + $dumper = ('workflow' === $type) ? new GraphvizDumper() : new StateMachineGraphvizDumper(); } $marking = new Marking(); @@ -90,14 +123,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $options = [ - 'name' => $serviceId, + 'name' => $workflowName, 'nofooter' => true, 'graph' => [ 'label' => $input->getOption('label'), ], ]; - $output->writeln($dumper->dump($workflow->getDefinition(), $marking, $options)); + $output->writeln($dumper->dump($workflow, $marking, $options)); return 0; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->workflows)); + } + + if ($input->mustSuggestOptionValuesFor('dump-format')) { + $suggestions->suggestValues(self::DUMP_FORMAT_OPTIONS); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php index a52177acc0471..b6b3a0222f9a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Translation\Command\XliffLintCommand as BaseLintCommand; /** @@ -22,10 +23,9 @@ * * @final */ +#[AsCommand(name: 'lint:xliff', description: 'Lints an XLIFF file and outputs encountered errors')] class XliffLintCommand extends BaseLintCommand { - protected static $defaultName = 'lint:xliff'; - public function __construct() { $directoryIteratorProvider = function ($directory, $default) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php index 86787361aa274..0686cde37ccba 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Yaml\Command\LintCommand as BaseLintCommand; /** @@ -21,10 +22,9 @@ * * @final */ +#[AsCommand(name: 'lint:yaml', description: 'Lint a YAML file and outputs encountered errors')] class YamlLintCommand extends BaseLintCommand { - protected static $defaultName = 'lint:yaml'; - public function __construct() { $directoryIteratorProvider = function ($directory, $default) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 20fb6e05f73de..1e942604c995e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -19,7 +19,6 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; @@ -31,8 +30,8 @@ class Application extends BaseApplication { private $kernel; - private $commandsRegistered = false; - private $registrationErrors = []; + private bool $commandsRegistered = false; + private array $registrationErrors = []; public function __construct(KernelInterface $kernel) { @@ -42,15 +41,13 @@ public function __construct(KernelInterface $kernel) $inputDefinition = $this->getDefinition(); $inputDefinition->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $kernel->getEnvironment())); - $inputDefinition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switches off debug mode.')); + $inputDefinition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switch off debug mode.')); } /** * Gets the Kernel associated with this Console. - * - * @return KernelInterface A KernelInterface instance */ - public function getKernel() + public function getKernel(): KernelInterface { return $this->kernel; } @@ -70,7 +67,7 @@ public function reset() * * @return int 0 if everything went fine, or an error code */ - public function doRun(InputInterface $input, OutputInterface $output) + public function doRun(InputInterface $input, OutputInterface $output): int { $this->registerCommands(); @@ -86,7 +83,7 @@ public function doRun(InputInterface $input, OutputInterface $output) /** * {@inheritdoc} */ - protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int { if (!$command instanceof ListCommand) { if ($this->registrationErrors) { @@ -110,7 +107,7 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI /** * {@inheritdoc} */ - public function find($name) + public function find(string $name): Command { $this->registerCommands(); @@ -120,7 +117,7 @@ public function find($name) /** * {@inheritdoc} */ - public function get($name) + public function get(string $name): Command { $this->registerCommands(); @@ -136,7 +133,7 @@ public function get($name) /** * {@inheritdoc} */ - public function all($namespace = null) + public function all(string $namespace = null): array { $this->registerCommands(); @@ -146,12 +143,12 @@ public function all($namespace = null) /** * {@inheritdoc} */ - public function getLongVersion() + public function getLongVersion(): string { - return parent::getLongVersion().sprintf(' (env: %s, debug: %s) #StandWithUkraine https://sf.to/ukraine', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); + return parent::getLongVersion().sprintf(' (env: %s, debug: %s) #StandWithUkraine https://sf.to/ukraine', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); } - public function add(Command $command) + public function add(Command $command): ?Command { $this->registerCommands(); @@ -207,15 +204,7 @@ private function renderRegistrationErrors(InputInterface $input, OutputInterface (new SymfonyStyle($input, $output))->warning('Some commands could not be registered:'); foreach ($this->registrationErrors as $error) { - if (method_exists($this, 'doRenderThrowable')) { - $this->doRenderThrowable($error, $output); - } else { - if (!$error instanceof \Exception) { - $error = new FatalThrowableError($error); - } - - $this->doRenderException($error, $output); - } + $this->doRenderThrowable($error, $output); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php index ab77fbf23b0c0..c6545f70abaa2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php @@ -38,7 +38,7 @@ abstract class Descriptor implements DescriptorInterface /** * {@inheritdoc} */ - public function describe(OutputInterface $output, $object, array $options = []) + public function describe(OutputInterface $output, mixed $object, array $options = []) { $this->output = $output; @@ -64,6 +64,9 @@ public function describe(OutputInterface $output, $object, array $options = []) case $object instanceof ContainerBuilder && isset($options['parameter']): $this->describeContainerParameter($object->resolveEnvPlaceholders($object->getParameter($options['parameter'])), $options); break; + case $object instanceof ContainerBuilder && isset($options['deprecations']): + $this->describeContainerDeprecations($object, $options); + break; case $object instanceof ContainerBuilder: $this->describeContainerServices($object, $options); break; @@ -80,7 +83,7 @@ public function describe(OutputInterface $output, $object, array $options = []) $this->describeCallable($object, $options); break; default: - throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', \get_class($object))); + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))); } } @@ -110,7 +113,7 @@ abstract protected function describeContainerTags(ContainerBuilder $builder, arr * * @param Definition|Alias|object $service */ - abstract protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null); + abstract protected function describeContainerService(object $service, array $options = [], ContainerBuilder $builder = null); /** * Describes container services. @@ -120,11 +123,13 @@ abstract protected function describeContainerService($service, array $options = */ abstract protected function describeContainerServices(ContainerBuilder $builder, array $options = []); + abstract protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void; + abstract protected function describeContainerDefinition(Definition $definition, array $options = []); abstract protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null); - abstract protected function describeContainerParameter($parameter, array $options = []); + abstract protected function describeContainerParameter(mixed $parameter, array $options = []); abstract protected function describeContainerEnvVars(array $envs, array $options = []); @@ -136,19 +141,9 @@ abstract protected function describeContainerEnvVars(array $envs, array $options */ abstract protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []); - /** - * Describes a callable. - * - * @param mixed $callable - */ - abstract protected function describeCallable($callable, array $options = []); + abstract protected function describeCallable(mixed $callable, array $options = []); - /** - * Formats a value as string. - * - * @param mixed $value - */ - protected function formatValue($value): string + protected function formatValue(mixed $value): string { if (\is_object($value)) { return sprintf('object(%s)', \get_class($value)); @@ -161,12 +156,7 @@ protected function formatValue($value): string return preg_replace("/\n\s*/s", '', var_export($value, true)); } - /** - * Formats a parameter. - * - * @param mixed $value - */ - protected function formatParameter($value): string + protected function formatParameter(mixed $value): string { if ($value instanceof \UnitEnum) { return var_export($value, true); @@ -195,10 +185,7 @@ protected function formatParameter($value): string return (string) $value; } - /** - * @return mixed - */ - protected function resolveServiceDefinition(ContainerBuilder $builder, string $serviceId) + protected function resolveServiceDefinition(ContainerBuilder $builder, string $serviceId): mixed { if ($builder->hasDefinition($serviceId)) { return $builder->getDefinition($serviceId); diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 0b38ebf31c0f4..c87f96393f600 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -12,7 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -65,7 +67,7 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio $this->writeData($data, $options); } - protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) + protected function describeContainerService(object $service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { throw new \InvalidArgumentException('An "id" option must be provided.'); @@ -134,15 +136,15 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { - $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, $options['event'] ?? null), $options); + $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, $options), $options); } - protected function describeCallable($callable, array $options = []) + protected function describeCallable(mixed $callable, array $options = []) { $this->writeData($this->getCallableData($callable), $options); } - protected function describeContainerParameter($parameter, array $options = []) + protected function describeContainerParameter(mixed $parameter, array $options = []) { $key = $options['parameter'] ?? ''; @@ -154,6 +156,30 @@ protected function describeContainerEnvVars(array $envs, array $options = []) throw new LogicException('Using the JSON format to debug environment variables is not supported.'); } + protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void + { + $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class')); + if (!file_exists($containerDeprecationFilePath)) { + throw new RuntimeException('The deprecation file does not exist, please try warming the cache first.'); + } + + $logs = unserialize(file_get_contents($containerDeprecationFilePath)); + + $formattedLogs = []; + $remainingCount = 0; + foreach ($logs as $log) { + $formattedLogs[] = [ + 'message' => $log['message'], + 'file' => $log['file'], + 'line' => $log['line'], + 'count' => $log['count'], + ]; + $remainingCount += $log['count']; + } + + $this->writeData(['remainingCount' => $remainingCount, 'deprecations' => $formattedLogs], $options); + } + private function writeData(array $data, array $options) { $flags = $options['json_encoding'] ?? 0; @@ -257,18 +283,19 @@ private function getContainerAliasData(Alias $alias): array ]; } - private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, string $event = null): array + private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, array $options): array { $data = []; + $event = \array_key_exists('event', $options) ? $options['event'] : null; - $registeredListeners = $eventDispatcher->getListeners($event); if (null !== $event) { - foreach ($registeredListeners as $listener) { + foreach ($eventDispatcher->getListeners($event) as $listener) { $l = $this->getCallableData($listener); $l['priority'] = $eventDispatcher->getListenerPriority($event, $listener); $data[] = $l; } } else { + $registeredListeners = \array_key_exists('events', $options) ? array_combine($options['events'], array_map(function ($event) use ($eventDispatcher) { return $eventDispatcher->getListeners($event); }, $options['events'])) : $eventDispatcher->getListeners(); ksort($registeredListeners); foreach ($registeredListeners as $eventListened => $eventListeners) { @@ -283,7 +310,7 @@ private function getEventDispatcherListenersData(EventDispatcherInterface $event return $data; } - private function getCallableData($callable): array + private function getCallableData(mixed $callable): array { $data = []; @@ -354,7 +381,7 @@ private function getCallableData($callable): array throw new \InvalidArgumentException('Callable is not describable.'); } - private function describeValue($value, bool $omitTags, bool $showArguments) + private function describeValue(mixed $value, bool $omitTags, bool $showArguments): mixed { if (\is_array($value)) { $data = []; @@ -376,6 +403,10 @@ private function describeValue($value, bool $omitTags, bool $showArguments) ]; } + if ($value instanceof AbstractArgument) { + return ['type' => 'abstract', 'text' => $value->getText()]; + } + if ($value instanceof ArgumentInterface) { return $this->describeValue($value->getValues(), $omitTags, $showArguments); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php index a2360a094ee9a..b70bfce81260e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -87,7 +88,7 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio } } - protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) + protected function describeContainerService(object $service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { throw new \InvalidArgumentException('An "id" option must be provided.'); @@ -104,6 +105,33 @@ protected function describeContainerService($service, array $options = [], Conta } } + protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void + { + $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class')); + if (!file_exists($containerDeprecationFilePath)) { + throw new RuntimeException('The deprecation file does not exist, please try warming the cache first.'); + } + + $logs = unserialize(file_get_contents($containerDeprecationFilePath)); + if (0 === \count($logs)) { + $this->write("## There are no deprecations in the logs!\n"); + + return; + } + + $formattedLogs = []; + $remainingCount = 0; + foreach ($logs as $log) { + $formattedLogs[] = sprintf("- %sx: \"%s\" in %s:%s\n", $log['count'], $log['message'], $log['file'], $log['line']); + $remainingCount += $log['count']; + } + + $this->write(sprintf("## Remaining deprecations (%s)\n\n", $remainingCount)); + foreach ($formattedLogs as $formattedLog) { + $this->write($formattedLog); + } + } + protected function describeContainerServices(ContainerBuilder $builder, array $options = []) { $showHidden = isset($options['show_hidden']) && $options['show_hidden']; @@ -246,7 +274,7 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias])); } - protected function describeContainerParameter($parameter, array $options = []) + protected function describeContainerParameter(mixed $parameter, array $options = []) { $this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', \strlen($options['parameter'])), $this->formatParameter($parameter)) : $parameter); } @@ -259,15 +287,24 @@ protected function describeContainerEnvVars(array $envs, array $options = []) protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { $event = $options['event'] ?? null; + $dispatcherServiceName = $options['dispatcher_service_name'] ?? null; $title = 'Registered listeners'; + + if (null !== $dispatcherServiceName) { + $title .= sprintf(' of event dispatcher "%s"', $dispatcherServiceName); + } + if (null !== $event) { $title .= sprintf(' for event `%s` ordered by descending priority', $event); + $registeredListeners = $eventDispatcher->getListeners($event); + } else { + // Try to see if "events" exists + $registeredListeners = \array_key_exists('events', $options) ? array_combine($options['events'], array_map(function ($event) use ($eventDispatcher) { return $eventDispatcher->getListeners($event); }, $options['events'])) : $eventDispatcher->getListeners(); } $this->write(sprintf('# %s', $title)."\n"); - $registeredListeners = $eventDispatcher->getListeners($event); if (null !== $event) { foreach ($registeredListeners as $order => $listener) { $this->write("\n".sprintf('## Listener %d', $order + 1)."\n"); @@ -289,7 +326,7 @@ protected function describeEventDispatcherListeners(EventDispatcherInterface $ev } } - protected function describeCallable($callable, array $options = []) + protected function describeCallable(mixed $callable, array $options = []) { $string = ''; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index 3441536ab8404..2706881a9aec4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; @@ -61,11 +62,11 @@ protected function describeRouteCollection(RouteCollection $routes, array $optio $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY', $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY', '' !== $route->getHost() ? $route->getHost() : 'ANY', - $this->formatControllerLink($controller, $route->getPath()), + $this->formatControllerLink($controller, $route->getPath(), $options['container'] ?? null), ]; if ($showControllers) { - $row[] = $controller ? $this->formatControllerLink($controller, $this->formatCallable($controller)) : ''; + $row[] = $controller ? $this->formatControllerLink($controller, $this->formatCallable($controller), $options['container'] ?? null) : ''; } $tableRows[] = $row; @@ -82,6 +83,11 @@ protected function describeRouteCollection(RouteCollection $routes, array $optio protected function describeRoute(Route $route, array $options = []) { + $defaults = $route->getDefaults(); + if (isset($defaults['_controller'])) { + $defaults['_controller'] = $this->formatControllerLink($defaults['_controller'], $this->formatCallable($defaults['_controller']), $options['container'] ?? null); + } + $tableHeaders = ['Property', 'Value']; $tableRows = [ ['Route Name', $options['name'] ?? ''], @@ -93,7 +99,7 @@ protected function describeRoute(Route $route, array $options = []) ['Method', ($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY')], ['Requirements', ($route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM')], ['Class', \get_class($route)], - ['Defaults', $this->formatRouterConfig($route->getDefaults())], + ['Defaults', $this->formatRouterConfig($defaults)], ['Options', $this->formatRouterConfig($route->getOptions())], ]; @@ -135,7 +141,7 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio } } - protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) + protected function describeContainerService(object $service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { throw new \InvalidArgumentException('An "id" option must be provided.'); @@ -228,7 +234,7 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o if (0 === $key) { $tableRows[] = array_merge([$serviceId], $tagValues, [$definition->getClass()]); } else { - $tableRows[] = array_merge([' "'], $tagValues, ['']); + $tableRows[] = array_merge([' (same service as previous, another tag)'], $tagValues, ['']); } } } else { @@ -344,6 +350,8 @@ protected function describeContainerDefinition(Definition $definition, array $op $argumentsInformation[] = 'Inlined Service'; } elseif ($argument instanceof \UnitEnum) { $argumentsInformation[] = var_export($argument, true); + } elseif ($argument instanceof AbstractArgument) { + $argumentsInformation[] = sprintf('Abstract argument (%s)', $argument->getText()); } else { $argumentsInformation[] = \is_array($argument) ? sprintf('Array (%d element(s))', \count($argument)) : $argument; } @@ -355,6 +363,32 @@ protected function describeContainerDefinition(Definition $definition, array $op $options['output']->table($tableHeaders, $tableRows); } + protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void + { + $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class')); + if (!file_exists($containerDeprecationFilePath)) { + $options['output']->warning('The deprecation file does not exist, please try warming the cache first.'); + + return; + } + + $logs = unserialize(file_get_contents($containerDeprecationFilePath)); + if (0 === \count($logs)) { + $options['output']->success('There are no deprecations in the logs!'); + + return; + } + + $formattedLogs = []; + $remainingCount = 0; + foreach ($logs as $log) { + $formattedLogs[] = sprintf("%sx: %s\n in %s:%s", $log['count'], $log['message'], $log['file'], $log['line']); + $remainingCount += $log['count']; + } + $options['output']->title(sprintf('Remaining deprecations (%s)', $remainingCount)); + $options['output']->listing($formattedLogs); + } + protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null) { if ($alias->isPublic() && !$alias->isPrivate()) { @@ -370,7 +404,7 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias])); } - protected function describeContainerParameter($parameter, array $options = []) + protected function describeContainerParameter(mixed $parameter, array $options = []) { $options['output']->table( ['Parameter', 'Value'], @@ -445,16 +479,24 @@ protected function describeContainerEnvVars(array $envs, array $options = []) protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { $event = $options['event'] ?? null; + $dispatcherServiceName = $options['dispatcher_service_name'] ?? null; + + $title = 'Registered Listeners'; + + if (null !== $dispatcherServiceName) { + $title .= sprintf(' of Event Dispatcher "%s"', $dispatcherServiceName); + } if (null !== $event) { - $title = sprintf('Registered Listeners for "%s" Event', $event); + $title .= sprintf(' for "%s" Event', $event); + $registeredListeners = $eventDispatcher->getListeners($event); } else { - $title = 'Registered Listeners Grouped by Event'; + $title .= ' Grouped by Event'; + // Try to see if "events" exists + $registeredListeners = \array_key_exists('events', $options) ? array_combine($options['events'], array_map(function ($event) use ($eventDispatcher) { return $eventDispatcher->getListeners($event); }, $options['events'])) : $eventDispatcher->getListeners(); } $options['output']->title($title); - - $registeredListeners = $eventDispatcher->getListeners($event); if (null !== $event) { $this->renderEventListenerTable($eventDispatcher, $event, $registeredListeners, $options['output']); } else { @@ -466,7 +508,7 @@ protected function describeEventDispatcherListeners(EventDispatcherInterface $ev } } - protected function describeCallable($callable, array $options = []) + protected function describeCallable(mixed $callable, array $options = []) { $this->writeText($this->formatCallable($callable), $options); } @@ -499,7 +541,7 @@ private function formatRouterConfig(array $config): string return trim($configAsString); } - private function formatControllerLink($controller, string $anchorText): string + private function formatControllerLink(mixed $controller, string $anchorText, callable $getContainer = null): string { if (null === $this->fileLinkFormatter) { return $anchorText; @@ -522,7 +564,27 @@ private function formatControllerLink($controller, string $anchorText): string $r = new \ReflectionFunction($controller); } } catch (\ReflectionException $e) { - return $anchorText; + if (\is_array($controller)) { + $controller = implode('::', $controller); + } + + $id = $controller; + $method = '__invoke'; + + if ($pos = strpos($controller, '::')) { + $id = substr($controller, 0, $pos); + $method = substr($controller, $pos + 2); + } + + if (!$getContainer || !($container = $getContainer()) || !$container->has($id)) { + return $anchorText; + } + + try { + $r = new \ReflectionMethod($container->findDefinition($id)->getClass(), $method); + } catch (\ReflectionException $e) { + return $anchorText; + } } $fileLink = $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine()); @@ -533,7 +595,7 @@ private function formatControllerLink($controller, string $anchorText): string return $anchorText; } - private function formatCallable($callable): string + private function formatCallable(mixed $callable): string { if (\is_array($callable)) { if (\is_object($callable[0])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index 44a79a8fa90bd..91984fb621784 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -12,7 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; @@ -51,7 +53,7 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio $this->writeDocument($this->getContainerTagsDocument($builder, isset($options['show_hidden']) && $options['show_hidden'])); } - protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) + protected function describeContainerService(object $service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { throw new \InvalidArgumentException('An "id" option must be provided.'); @@ -88,15 +90,15 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { - $this->writeDocument($this->getEventDispatcherListenersDocument($eventDispatcher, $options['event'] ?? null)); + $this->writeDocument($this->getEventDispatcherListenersDocument($eventDispatcher, $options)); } - protected function describeCallable($callable, array $options = []) + protected function describeCallable(mixed $callable, array $options = []) { $this->writeDocument($this->getCallableDocument($callable)); } - protected function describeContainerParameter($parameter, array $options = []) + protected function describeContainerParameter(mixed $parameter, array $options = []) { $this->writeDocument($this->getContainerParameterDocument($parameter, $options)); } @@ -106,6 +108,34 @@ protected function describeContainerEnvVars(array $envs, array $options = []) throw new LogicException('Using the XML format to debug environment variables is not supported.'); } + protected function describeContainerDeprecations(ContainerBuilder $builder, array $options = []): void + { + $containerDeprecationFilePath = sprintf('%s/%sDeprecations.log', $builder->getParameter('kernel.build_dir'), $builder->getParameter('kernel.container_class')); + if (!file_exists($containerDeprecationFilePath)) { + throw new RuntimeException('The deprecation file does not exist, please try warming the cache first.'); + } + + $logs = unserialize(file_get_contents($containerDeprecationFilePath)); + + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($deprecationsXML = $dom->createElement('deprecations')); + + $formattedLogs = []; + $remainingCount = 0; + foreach ($logs as $log) { + $deprecationsXML->appendChild($deprecationXML = $dom->createElement('deprecation')); + $deprecationXML->setAttribute('count', $log['count']); + $deprecationXML->appendChild($dom->createElement('message', $log['message'])); + $deprecationXML->appendChild($dom->createElement('file', $log['file'])); + $deprecationXML->appendChild($dom->createElement('line', $log['line'])); + $remainingCount += $log['count']; + } + + $deprecationsXML->setAttribute('remainingCount', $remainingCount); + + $this->writeDocument($dom); + } + private function writeDocument(\DOMDocument $dom) { $dom->formatOutput = true; @@ -225,7 +255,7 @@ private function getContainerTagsDocument(ContainerBuilder $builder, bool $showH return $dom; } - private function getContainerServiceDocument($service, string $id, ContainerBuilder $builder = null, bool $showArguments = false): \DOMDocument + private function getContainerServiceDocument(object $service, string $id, ContainerBuilder $builder = null, bool $showArguments = false): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); @@ -380,6 +410,9 @@ private function getArgumentNodes(array $arguments, \DOMDocument $dom): array } } elseif ($argument instanceof Definition) { $argumentXML->appendChild($dom->importNode($this->getContainerDefinitionDocument($argument, null, false, true)->childNodes->item(0), true)); + } elseif ($argument instanceof AbstractArgument) { + $argumentXML->setAttribute('type', 'abstract'); + $argumentXML->appendChild(new \DOMText($argument->getText())); } elseif (\is_array($argument)) { $argumentXML->setAttribute('type', 'collection'); @@ -414,7 +447,7 @@ private function getContainerAliasDocument(Alias $alias, string $id = null): \DO return $dom; } - private function getContainerParameterDocument($parameter, array $options = []): \DOMDocument + private function getContainerParameterDocument(mixed $parameter, array $options = []): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($parameterXML = $dom->createElement('parameter')); @@ -428,15 +461,18 @@ private function getContainerParameterDocument($parameter, array $options = []): return $dom; } - private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, string $event = null): \DOMDocument + private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, array $options): \DOMDocument { + $event = \array_key_exists('event', $options) ? $options['event'] : null; $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($eventDispatcherXML = $dom->createElement('event-dispatcher')); - $registeredListeners = $eventDispatcher->getListeners($event); if (null !== $event) { + $registeredListeners = $eventDispatcher->getListeners($event); $this->appendEventListenerDocument($eventDispatcher, $event, $eventDispatcherXML, $registeredListeners); } else { + // Try to see if "events" exists + $registeredListeners = \array_key_exists('events', $options) ? array_combine($options['events'], array_map(function ($event) use ($eventDispatcher) { return $eventDispatcher->getListeners($event); }, $options['events'])) : $eventDispatcher->getListeners(); ksort($registeredListeners); foreach ($registeredListeners as $eventListened => $eventListeners) { @@ -460,7 +496,7 @@ private function appendEventListenerDocument(EventDispatcherInterface $eventDisp } } - private function getCallableDocument($callable): \DOMDocument + private function getCallableDocument(mixed $callable): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($callableXML = $dom->createElement('callable')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php index 5a2112daf9ed2..9e0836101b38e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php @@ -11,40 +11,53 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; -use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; +use Psr\Link\LinkInterface; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Templating\EngineInterface; +use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; use Symfony\Contracts\Service\ServiceSubscriberInterface; use Twig\Environment; /** - * Provides common features needed in controllers. + * Provides shortcuts for HTTP-related features in controllers. * * @author Fabien Potencier */ abstract class AbstractController implements ServiceSubscriberInterface { - use ControllerTrait; - /** * @var ContainerInterface */ protected $container; /** - * @internal * @required */ public function setContainer(ContainerInterface $container): ?ContainerInterface @@ -57,12 +70,8 @@ public function setContainer(ContainerInterface $container): ?ContainerInterface /** * Gets a container parameter by its name. - * - * @return array|bool|float|int|string|\UnitEnum|null - * - * @final */ - protected function getParameter(string $name) + protected function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null { if (!$this->container->has('parameter_bag')) { throw new ServiceNotFoundException('parameter_bag.', null, null, [], sprintf('The "%s::getParameter()" method is missing a parameter bag to work properly. Did you forget to register your controller as a service subscriber? This can be fixed either by using autoconfiguration or by manually wiring a "parameter_bag" in the service locator passed to the controller.', static::class)); @@ -71,24 +80,314 @@ protected function getParameter(string $name) return $this->container->get('parameter_bag')->get($name); } - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'router' => '?'.RouterInterface::class, 'request_stack' => '?'.RequestStack::class, 'http_kernel' => '?'.HttpKernelInterface::class, 'serializer' => '?'.SerializerInterface::class, - 'session' => '?'.SessionInterface::class, 'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class, - 'templating' => '?'.EngineInterface::class, 'twig' => '?'.Environment::class, - 'doctrine' => '?'.ManagerRegistry::class, 'form.factory' => '?'.FormFactoryInterface::class, 'security.token_storage' => '?'.TokenStorageInterface::class, 'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class, 'parameter_bag' => '?'.ContainerBagInterface::class, - 'message_bus' => '?'.MessageBusInterface::class, - 'messenger.default_bus' => '?'.MessageBusInterface::class, ]; } + + /** + * Generates a URL from the given parameters. + * + * @see UrlGeneratorInterface + */ + protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string + { + return $this->container->get('router')->generate($route, $parameters, $referenceType); + } + + /** + * Forwards the request to another controller. + * + * @param string $controller The controller name (a string like Bundle\BlogBundle\Controller\PostController::indexAction) + */ + protected function forward(string $controller, array $path = [], array $query = []): Response + { + $request = $this->container->get('request_stack')->getCurrentRequest(); + $path['_controller'] = $controller; + $subRequest = $request->duplicate($query, null, $path); + + return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } + + /** + * Returns a RedirectResponse to the given URL. + */ + protected function redirect(string $url, int $status = 302): RedirectResponse + { + return new RedirectResponse($url, $status); + } + + /** + * Returns a RedirectResponse to the given route with the given parameters. + */ + protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse + { + return $this->redirect($this->generateUrl($route, $parameters), $status); + } + + /** + * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. + */ + protected function json(mixed $data, int $status = 200, array $headers = [], array $context = []): JsonResponse + { + if ($this->container->has('serializer')) { + $json = $this->container->get('serializer')->serialize($data, 'json', array_merge([ + 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, + ], $context)); + + return new JsonResponse($json, $status, $headers, true); + } + + return new JsonResponse($data, $status, $headers); + } + + /** + * Returns a BinaryFileResponse object with original or customized file name and disposition header. + */ + protected function file(\SplFileInfo|string $file, string $fileName = null, string $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT): BinaryFileResponse + { + $response = new BinaryFileResponse($file); + $response->setContentDisposition($disposition, null === $fileName ? $response->getFile()->getFilename() : $fileName); + + return $response; + } + + /** + * Adds a flash message to the current session for type. + * + * @throws \LogicException + */ + protected function addFlash(string $type, mixed $message): void + { + try { + $this->container->get('request_stack')->getSession()->getFlashBag()->add($type, $message); + } catch (SessionNotFoundException $e) { + throw new \LogicException('You cannot use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e); + } + } + + /** + * Checks if the attribute is granted against the current authentication token and optionally supplied subject. + * + * @throws \LogicException + */ + protected function isGranted(mixed $attribute, mixed $subject = null): bool + { + if (!$this->container->has('security.authorization_checker')) { + throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); + } + + return $this->container->get('security.authorization_checker')->isGranted($attribute, $subject); + } + + /** + * Throws an exception unless the attribute is granted against the current authentication token and optionally + * supplied subject. + * + * @throws AccessDeniedException + */ + protected function denyAccessUnlessGranted(mixed $attribute, mixed $subject = null, string $message = 'Access Denied.'): void + { + if (!$this->isGranted($attribute, $subject)) { + $exception = $this->createAccessDeniedException($message); + $exception->setAttributes($attribute); + $exception->setSubject($subject); + + throw $exception; + } + } + + /** + * Returns a rendered view. + */ + protected function renderView(string $view, array $parameters = []): string + { + if (!$this->container->has('twig')) { + throw new \LogicException('You cannot use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".'); + } + + return $this->container->get('twig')->render($view, $parameters); + } + + /** + * Renders a view. + */ + protected function render(string $view, array $parameters = [], Response $response = null): Response + { + $content = $this->renderView($view, $parameters); + + if (null === $response) { + $response = new Response(); + } + + $response->setContent($content); + + return $response; + } + + /** + * Renders a view and sets the appropriate status code when a form is listed in parameters. + * + * If an invalid form is found in the list of parameters, a 422 status code is returned. + */ + protected function renderForm(string $view, array $parameters = [], Response $response = null): Response + { + if (null === $response) { + $response = new Response(); + } + + foreach ($parameters as $k => $v) { + if ($v instanceof FormView) { + throw new \LogicException(sprintf('Passing a FormView to "%s::renderForm()" is not supported, pass directly the form instead for parameter "%s".', get_debug_type($this), $k)); + } + + if (!$v instanceof FormInterface) { + continue; + } + + $parameters[$k] = $v->createView(); + + if (200 === $response->getStatusCode() && $v->isSubmitted() && !$v->isValid()) { + $response->setStatusCode(422); + } + } + + return $this->render($view, $parameters, $response); + } + + /** + * Streams a view. + */ + protected function stream(string $view, array $parameters = [], StreamedResponse $response = null): StreamedResponse + { + if (!$this->container->has('twig')) { + throw new \LogicException('You cannot use the "stream" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".'); + } + + $twig = $this->container->get('twig'); + + $callback = function () use ($twig, $view, $parameters) { + $twig->display($view, $parameters); + }; + + if (null === $response) { + return new StreamedResponse($callback); + } + + $response->setCallback($callback); + + return $response; + } + + /** + * Returns a NotFoundHttpException. + * + * This will result in a 404 response code. Usage example: + * + * throw $this->createNotFoundException('Page not found!'); + */ + protected function createNotFoundException(string $message = 'Not Found', \Throwable $previous = null): NotFoundHttpException + { + return new NotFoundHttpException($message, $previous); + } + + /** + * Returns an AccessDeniedException. + * + * This will result in a 403 response code. Usage example: + * + * throw $this->createAccessDeniedException('Unable to access this page!'); + * + * @throws \LogicException If the Security component is not available + */ + protected function createAccessDeniedException(string $message = 'Access Denied.', \Throwable $previous = null): AccessDeniedException + { + if (!class_exists(AccessDeniedException::class)) { + throw new \LogicException('You cannot use the "createAccessDeniedException" method if the Security component is not available. Try running "composer require symfony/security-bundle".'); + } + + return new AccessDeniedException($message, $previous); + } + + /** + * Creates and returns a Form instance from the type of the form. + */ + protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface + { + return $this->container->get('form.factory')->create($type, $data, $options); + } + + /** + * Creates and returns a form builder instance. + */ + protected function createFormBuilder(mixed $data = null, array $options = []): FormBuilderInterface + { + return $this->container->get('form.factory')->createBuilder(FormType::class, $data, $options); + } + + /** + * Get a user from the Security Token Storage. + * + * @throws \LogicException If SecurityBundle is not available + * + * @see TokenInterface::getUser() + */ + protected function getUser(): ?UserInterface + { + if (!$this->container->has('security.token_storage')) { + throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); + } + + if (null === $token = $this->container->get('security.token_storage')->getToken()) { + return null; + } + + return $token->getUser(); + } + + /** + * Checks the validity of a CSRF token. + * + * @param string $id The id used when generating the token + * @param string|null $token The actual token sent with the request that should be validated + */ + protected function isCsrfTokenValid(string $id, ?string $token): bool + { + if (!$this->container->has('security.csrf.token_manager')) { + throw new \LogicException('CSRF protection is not enabled in your application. Enable it with the "csrf_protection" key in "config/packages/framework.yaml".'); + } + + return $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($id, $token)); + } + + /** + * Adds a Link HTTP header to the current response. + * + * @see https://tools.ietf.org/html/rfc5988 + */ + protected function addLink(Request $request, LinkInterface $link): void + { + if (!class_exists(AddLinkHeaderListener::class)) { + throw new \LogicException('You cannot use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); + } + + if (null === $linkProvider = $request->attributes->get('_links')) { + $request->attributes->set('_links', new GenericLinkProvider([$link])); + + return; + } + + $request->attributes->set('_links', $linkProvider->withLink($link)); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php deleted file mode 100644 index a7d8f9425c0d0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Controller; - -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; - -/** - * Controller is a simple implementation of a Controller. - * - * It provides methods to common features needed in controllers. - * - * @deprecated since Symfony 4.2, use "Symfony\Bundle\FrameworkBundle\Controller\AbstractController" instead. - * - * @author Fabien Potencier - */ -abstract class Controller implements ContainerAwareInterface -{ - use ContainerAwareTrait; - use ControllerTrait; - - /** - * Gets a container configuration parameter by its name. - * - * @return array|bool|float|int|string|\UnitEnum|null - * - * @final - */ - protected function getParameter(string $name) - { - return $this->container->getParameter($name); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php deleted file mode 100644 index 34e4382ad8f96..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Controller; - -use Symfony\Component\HttpKernel\Bundle\BundleInterface; -use Symfony\Component\HttpKernel\KernelInterface; - -/** - * ControllerNameParser converts controller from the short notation a:b:c - * (BlogBundle:Post:index) to a fully-qualified class::method string - * (Bundle\BlogBundle\Controller\PostController::indexAction). - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.1 - */ -class ControllerNameParser -{ - protected $kernel; - - public function __construct(KernelInterface $kernel, bool $triggerDeprecation = true) - { - $this->kernel = $kernel; - - if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); - } - } - - /** - * Converts a short notation a:b:c to a class::method. - * - * @param string $controller A short notation controller (a:b:c) - * - * @return string A string in the class::method notation - * - * @throws \InvalidArgumentException when the specified bundle is not enabled - * or the controller cannot be found - */ - public function parse($controller) - { - if (2 > \func_num_args() || func_get_arg(1)) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); - } - - $parts = explode(':', $controller); - if (3 !== \count($parts) || \in_array('', $parts, true)) { - throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "a:b:c" controller string.', $controller)); - } - - $originalController = $controller; - [$bundleName, $controller, $action] = $parts; - $controller = str_replace('/', '\\', $controller); - - try { - // this throws an exception if there is no such bundle - $bundle = $this->kernel->getBundle($bundleName); - } catch (\InvalidArgumentException $e) { - $message = sprintf( - 'The "%s" (from the _controller value "%s") does not exist or is not enabled in your kernel!', - $bundleName, - $originalController - ); - - if ($alternative = $this->findAlternative($bundleName)) { - $message .= sprintf(' Did you mean "%s:%s:%s"?', $alternative, $controller, $action); - } - - throw new \InvalidArgumentException($message, 0, $e); - } - - $try = $bundle->getNamespace().'\\Controller\\'.$controller.'Controller'; - if (class_exists($try)) { - return $try.'::'.$action.'Action'; - } - - throw new \InvalidArgumentException(sprintf('The _controller value "%s:%s:%s" maps to a "%s" class, but this class was not found. Create this class or check the spelling of the class and its namespace.', $bundleName, $controller, $action, $try)); - } - - /** - * Converts a class::method notation to a short one (a:b:c). - * - * @param string $controller A string in the class::method notation - * - * @return string A short notation controller (a:b:c) - * - * @throws \InvalidArgumentException when the controller is not valid or cannot be found in any bundle - */ - public function build($controller) - { - @trigger_error(sprintf('The %s class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); - - if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controller, $match)) { - throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "class::method" string.', $controller)); - } - - $className = $match[1]; - $controllerName = $match[2]; - $actionName = $match[3]; - foreach ($this->kernel->getBundles() as $name => $bundle) { - if (!str_starts_with($className, $bundle->getNamespace())) { - continue; - } - - return sprintf('%s:%s:%s', $name, $controllerName, $actionName); - } - - throw new \InvalidArgumentException(sprintf('Unable to find a bundle that defines controller "%s".', $controller)); - } - - /** - * Attempts to find a bundle that is *similar* to the given bundle name. - */ - private function findAlternative(string $nonExistentBundleName): ?string - { - $bundleNames = array_map(function (BundleInterface $b) { - return $b->getName(); - }, $this->kernel->getBundles()); - - $alternative = null; - $shortest = null; - foreach ($bundleNames as $bundleName) { - // if there's a partial match, return it immediately - if (str_contains($bundleName, $nonExistentBundleName)) { - return $bundleName; - } - - $lev = levenshtein($nonExistentBundleName, $bundleName); - if ($lev <= \strlen($nonExistentBundleName) / 3 && (null === $alternative || $lev < $shortest)) { - $alternative = $bundleName; - $shortest = $lev; - } - } - - return $alternative; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php index 2883c80b1b0ba..0539c1ee734ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php @@ -11,61 +11,20 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; -use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver; /** * @author Fabien Potencier * - * @final since Symfony 4.4 + * @final */ class ControllerResolver extends ContainerControllerResolver { - /** - * @deprecated since Symfony 4.4 - */ - protected $parser; - - /** - * @param LoggerInterface|null $logger - */ - public function __construct(ContainerInterface $container, $logger = null) - { - if ($logger instanceof ControllerNameParser) { - @trigger_error(sprintf('Passing a "%s" instance as 2nd argument to "%s()" is deprecated since Symfony 4.4, pass a "%s" instance or null instead.', ControllerNameParser::class, __METHOD__, LoggerInterface::class), \E_USER_DEPRECATED); - $this->parser = $logger; - $logger = 2 < \func_num_args() ? func_get_arg(2) : null; - } elseif (2 < \func_num_args() && func_get_arg(2) instanceof ControllerNameParser) { - $this->parser = func_get_arg(2); - } elseif ($logger && !$logger instanceof LoggerInterface) { - throw new \TypeError(sprintf('Argument 2 of "%s()" must be an instance of "%s" or null, "%s" given.', __METHOD__, LoggerInterface::class, \is_object($logger) ? \get_class($logger) : \gettype($logger)), \E_USER_DEPRECATED); - } - - parent::__construct($container, $logger); - } - - /** - * {@inheritdoc} - */ - protected function createController($controller) - { - if ($this->parser && !str_contains($controller, '::') && 2 === substr_count($controller, ':')) { - // controller in the a:b:c notation then - $deprecatedNotation = $controller; - $controller = $this->parser->parse($deprecatedNotation, false); - - @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1. Use %s instead.', $deprecatedNotation, $controller), \E_USER_DEPRECATED); - } - - return parent::createController($controller); - } - /** * {@inheritdoc} */ - protected function instantiateController($class) + protected function instantiateController(string $class): object { $controller = parent::instantiateController($class); @@ -74,9 +33,7 @@ protected function instantiateController($class) } if ($controller instanceof AbstractController) { if (null === $previousContainer = $controller->setContainer($this->container)) { - @trigger_error(sprintf('Auto-injection of the container for "%s" is deprecated since Symfony 4.2. Configure it as a service instead.', $class), \E_USER_DEPRECATED); - // To be uncommented on Symfony 5: - //throw new \LogicException(sprintf('"%s" has no container set, did you forget to define it as a service subscriber?', $class)); + throw new \LogicException(sprintf('"%s" has no container set, did you forget to define it as a service subscriber?', $class)); } else { $controller->setContainer($previousContainer); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php deleted file mode 100644 index 8845e03d9a236..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ /dev/null @@ -1,440 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Controller; - -use Doctrine\Persistence\ManagerRegistry; -use Psr\Container\ContainerInterface; -use Psr\Link\LinkInterface; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Stamp\StampInterface; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Csrf\CsrfToken; -use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; -use Symfony\Component\WebLink\GenericLinkProvider; - -/** - * Common features needed in controllers. - * - * @author Fabien Potencier - * - * @internal - * - * @property ContainerInterface $container - */ -trait ControllerTrait -{ - /** - * Returns true if the service id is defined. - * - * @final - */ - protected function has(string $id): bool - { - return $this->container->has($id); - } - - /** - * Gets a container service by its id. - * - * @return object The service - * - * @final - */ - protected function get(string $id) - { - return $this->container->get($id); - } - - /** - * Generates a URL from the given parameters. - * - * @see UrlGeneratorInterface - * - * @final - */ - protected function generateUrl(string $route, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string - { - return $this->container->get('router')->generate($route, $parameters, $referenceType); - } - - /** - * Forwards the request to another controller. - * - * @param string $controller The controller name (a string like Bundle\BlogBundle\Controller\PostController::indexAction) - * - * @final - */ - protected function forward(string $controller, array $path = [], array $query = []): Response - { - $request = $this->container->get('request_stack')->getCurrentRequest(); - $path['_controller'] = $controller; - $subRequest = $request->duplicate($query, null, $path); - - return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST); - } - - /** - * Returns a RedirectResponse to the given URL. - * - * @final - */ - protected function redirect(string $url, int $status = 302): RedirectResponse - { - return new RedirectResponse($url, $status); - } - - /** - * Returns a RedirectResponse to the given route with the given parameters. - * - * @final - */ - protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse - { - return $this->redirect($this->generateUrl($route, $parameters), $status); - } - - /** - * Returns a JsonResponse that uses the serializer component if enabled, or json_encode. - * - * @final - */ - protected function json($data, int $status = 200, array $headers = [], array $context = []): JsonResponse - { - if ($this->container->has('serializer')) { - $json = $this->container->get('serializer')->serialize($data, 'json', array_merge([ - 'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS, - ], $context)); - - return new JsonResponse($json, $status, $headers, true); - } - - return new JsonResponse($data, $status, $headers); - } - - /** - * Returns a BinaryFileResponse object with original or customized file name and disposition header. - * - * @param \SplFileInfo|string $file File object or path to file to be sent as response - * - * @final - */ - protected function file($file, string $fileName = null, string $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT): BinaryFileResponse - { - $response = new BinaryFileResponse($file); - $response->setContentDisposition($disposition, $fileName ?? $response->getFile()->getFilename()); - - return $response; - } - - /** - * Adds a flash message to the current session for type. - * - * @throws \LogicException - * - * @final - */ - protected function addFlash(string $type, $message) - { - if (!$this->container->has('session')) { - throw new \LogicException('You can not use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".'); - } - - $this->container->get('session')->getFlashBag()->add($type, $message); - } - - /** - * Checks if the attributes are granted against the current authentication token and optionally supplied subject. - * - * @throws \LogicException - * - * @final - */ - protected function isGranted($attributes, $subject = null): bool - { - if (!$this->container->has('security.authorization_checker')) { - throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); - } - - return $this->container->get('security.authorization_checker')->isGranted($attributes, $subject); - } - - /** - * Throws an exception unless the attributes are granted against the current authentication token and optionally - * supplied subject. - * - * @throws AccessDeniedException - * - * @final - */ - protected function denyAccessUnlessGranted($attributes, $subject = null, string $message = 'Access Denied.') - { - if (!$this->isGranted($attributes, $subject)) { - $exception = $this->createAccessDeniedException($message); - $exception->setAttributes($attributes); - $exception->setSubject($subject); - - throw $exception; - } - } - - /** - * Returns a rendered view. - * - * @final - */ - protected function renderView(string $view, array $parameters = []): string - { - if ($this->container->has('templating') && $this->container->get('templating')->supports($view)) { - @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - - return $this->container->get('templating')->render($view, $parameters); - } - - if (!$this->container->has('twig')) { - throw new \LogicException('You can not use the "renderView" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".'); - } - - return $this->container->get('twig')->render($view, $parameters); - } - - /** - * Renders a view. - * - * @final - */ - protected function render(string $view, array $parameters = [], Response $response = null): Response - { - if ($this->container->has('templating') && $this->container->get('templating')->supports($view)) { - @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - - $content = $this->container->get('templating')->render($view, $parameters); - } elseif ($this->container->has('twig')) { - $content = $this->container->get('twig')->render($view, $parameters); - } else { - throw new \LogicException('You can not use the "render" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".'); - } - - if (null === $response) { - $response = new Response(); - } - - $response->setContent($content); - - return $response; - } - - /** - * Streams a view. - * - * @final - */ - protected function stream(string $view, array $parameters = [], StreamedResponse $response = null): StreamedResponse - { - if ($this->container->has('templating')) { - @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - - $templating = $this->container->get('templating'); - - $callback = function () use ($templating, $view, $parameters) { - $templating->stream($view, $parameters); - }; - } elseif ($this->container->has('twig')) { - $twig = $this->container->get('twig'); - - $callback = function () use ($twig, $view, $parameters) { - $twig->display($view, $parameters); - }; - } else { - throw new \LogicException('You can not use the "stream" method if the Templating Component or the Twig Bundle are not available. Try running "composer require symfony/twig-bundle".'); - } - - if (null === $response) { - return new StreamedResponse($callback); - } - - $response->setCallback($callback); - - return $response; - } - - /** - * Returns a NotFoundHttpException. - * - * This will result in a 404 response code. Usage example: - * - * throw $this->createNotFoundException('Page not found!'); - * - * @final - */ - protected function createNotFoundException(string $message = 'Not Found', \Throwable $previous = null): NotFoundHttpException - { - return new NotFoundHttpException($message, $previous); - } - - /** - * Returns an AccessDeniedException. - * - * This will result in a 403 response code. Usage example: - * - * throw $this->createAccessDeniedException('Unable to access this page!'); - * - * @throws \LogicException If the Security component is not available - * - * @final - */ - protected function createAccessDeniedException(string $message = 'Access Denied.', \Throwable $previous = null): AccessDeniedException - { - if (!class_exists(AccessDeniedException::class)) { - throw new \LogicException('You can not use the "createAccessDeniedException" method if the Security component is not available. Try running "composer require symfony/security-bundle".'); - } - - return new AccessDeniedException($message, $previous); - } - - /** - * Creates and returns a Form instance from the type of the form. - * - * @final - */ - protected function createForm(string $type, $data = null, array $options = []): FormInterface - { - return $this->container->get('form.factory')->create($type, $data, $options); - } - - /** - * Creates and returns a form builder instance. - * - * @final - */ - protected function createFormBuilder($data = null, array $options = []): FormBuilderInterface - { - return $this->container->get('form.factory')->createBuilder(FormType::class, $data, $options); - } - - /** - * Shortcut to return the Doctrine Registry service. - * - * @return ManagerRegistry - * - * @throws \LogicException If DoctrineBundle is not available - * - * @final - */ - protected function getDoctrine() - { - if (!$this->container->has('doctrine')) { - throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".'); - } - - return $this->container->get('doctrine'); - } - - /** - * Get a user from the Security Token Storage. - * - * @return UserInterface|object|null - * - * @throws \LogicException If SecurityBundle is not available - * - * @see TokenInterface::getUser() - * - * @final - */ - protected function getUser() - { - if (!$this->container->has('security.token_storage')) { - throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".'); - } - - if (null === $token = $this->container->get('security.token_storage')->getToken()) { - return null; - } - - if (!\is_object($user = $token->getUser())) { - // e.g. anonymous authentication - return null; - } - - return $user; - } - - /** - * Checks the validity of a CSRF token. - * - * @param string $id The id used when generating the token - * @param string|null $token The actual token sent with the request that should be validated - * - * @final - */ - protected function isCsrfTokenValid(string $id, ?string $token): bool - { - if (!$this->container->has('security.csrf.token_manager')) { - throw new \LogicException('CSRF protection is not enabled in your application. Enable it with the "csrf_protection" key in "config/packages/framework.yaml".'); - } - - return $this->container->get('security.csrf.token_manager')->isTokenValid(new CsrfToken($id, $token)); - } - - /** - * Dispatches a message to the bus. - * - * @param object|Envelope $message The message or the message pre-wrapped in an envelope - * @param StampInterface[] $stamps - * - * @final - */ - protected function dispatchMessage($message, array $stamps = []): Envelope - { - if (!$this->container->has('messenger.default_bus')) { - $message = class_exists(Envelope::class) ? 'You need to define the "messenger.default_bus" configuration option.' : 'Try running "composer require symfony/messenger".'; - throw new \LogicException('The message bus is not enabled in your application. '.$message); - } - - return $this->container->get('messenger.default_bus')->dispatch($message, $stamps); - } - - /** - * Adds a Link HTTP header to the current response. - * - * @see https://tools.ietf.org/html/rfc5988 - * - * @final - */ - protected function addLink(Request $request, LinkInterface $link) - { - if (!class_exists(AddLinkHeaderListener::class)) { - throw new \LogicException('You can not use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); - } - - if (null === $linkProvider = $request->attributes->get('_links')) { - $request->attributes->set('_links', new GenericLinkProvider([$link])); - - return; - } - - $request->attributes->set('_links', $linkProvider->withLink($link)); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index 109e83b6967ba..ea1517b9eeff5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; +use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -27,8 +28,8 @@ class RedirectController { private $router; - private $httpPort; - private $httpsPort; + private ?int $httpPort; + private ?int $httpsPort; public function __construct(UrlGeneratorInterface $router = null, int $httpPort = null, int $httpsPort = null) { @@ -53,7 +54,7 @@ public function __construct(UrlGeneratorInterface $router = null, int $httpPort * * @throws HttpException In case the route name is empty */ - public function redirectAction(Request $request, string $route, bool $permanent = false, $ignoreAttributes = false, bool $keepRequestMethod = false, bool $keepQueryParams = false): Response + public function redirectAction(Request $request, string $route, bool $permanent = false, bool|array $ignoreAttributes = false, bool $keepRequestMethod = false, bool $keepQueryParams = false): Response { if ('' == $route) { throw new HttpException($permanent ? 410 : 404); @@ -65,7 +66,7 @@ public function redirectAction(Request $request, string $route, bool $permanent if ($keepQueryParams) { if ($query = $request->server->get('QUERY_STRING')) { - $query = self::parseQuery($query); + $query = HeaderUtils::parseQuery($query); } else { $query = $request->query->all(); } @@ -185,49 +186,4 @@ public function __invoke(Request $request): Response throw new \RuntimeException(sprintf('The parameter "path" or "route" is required to configure the redirect action in "%s" routing configuration.', $request->attributes->get('_route'))); } - - private static function parseQuery(string $query) - { - $q = []; - - foreach (explode('&', $query) as $v) { - if (false !== $i = strpos($v, "\0")) { - $v = substr($v, 0, $i); - } - - if (false === $i = strpos($v, '=')) { - $k = urldecode($v); - $v = ''; - } else { - $k = urldecode(substr($v, 0, $i)); - $v = substr($v, $i); - } - - if (false !== $i = strpos($k, "\0")) { - $k = substr($k, 0, $i); - } - - $k = ltrim($k, ' '); - - if (false === $i = strpos($k, '[')) { - $q[] = bin2hex($k).$v; - } else { - $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; - } - } - - parse_str(implode('&', $q), $q); - - $query = []; - - foreach ($q as $k => $v) { - if (false !== $i = strpos($k, '_')) { - $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; - } else { - $query[hex2bin($k)] = $v; - } - } - - return $query; - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php index 705adc8765a26..2283dbc91fccf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Templating\EngineInterface; use Twig\Environment; /** @@ -25,37 +24,31 @@ class TemplateController { private $twig; - private $templating; - public function __construct(Environment $twig = null, EngineInterface $templating = null) + public function __construct(Environment $twig = null) { - if (null !== $templating) { - @trigger_error(sprintf('Using a "%s" instance for "%s" is deprecated since version 4.4; use a \Twig\Environment instance instead.', EngineInterface::class, __CLASS__), \E_USER_DEPRECATED); - } - $this->twig = $twig; - $this->templating = $templating; } /** * Renders a template. * - * @param string $template The template name - * @param int|null $maxAge Max age for client caching - * @param int|null $sharedAge Max age for shared (proxy) caching - * @param bool|null $private Whether or not caching should apply for client caches only + * @param string $template The template name + * @param int|null $maxAge Max age for client caching + * @param int|null $sharedAge Max age for shared (proxy) caching + * @param bool|null $private Whether or not caching should apply for client caches only + * @param array $context The context (arguments) of the template + * @param int $statusCode The HTTP status code to return with the response. Defaults to 200 */ - public function templateAction(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null): Response + public function templateAction(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null, array $context = [], int $statusCode = 200): Response { - if ($this->templating) { - $response = new Response($this->templating->render($template)); - } elseif ($this->twig) { - $response = new Response($this->twig->render($template)); - } else { - throw new \LogicException('You can not use the TemplateController if the Templating Component or the Twig Bundle are not available.'); + if (null === $this->twig) { + throw new \LogicException('You cannot use the TemplateController if the Twig Bundle is not available.'); } - if (null !== $maxAge) { + $response = new Response($this->twig->render($template, $context), $statusCode); + + if ($maxAge) { $response->setMaxAge($maxAge); } @@ -72,8 +65,8 @@ public function templateAction(string $template, int $maxAge = null, int $shared return $response; } - public function __invoke(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null): Response + public function __invoke(string $template, int $maxAge = null, int $sharedAge = null, bool $private = null, array $context = [], int $statusCode = 200): Response { - return $this->templateAction($template, $maxAge, $sharedAge, $private); + return $this->templateAction($template, $maxAge, $sharedAge, $private, $context, $statusCode); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/AbstractDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AbstractDataCollector.php new file mode 100644 index 0000000000000..7fa1ee2d3edb6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/AbstractDataCollector.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DataCollector; + +use Symfony\Component\HttpKernel\DataCollector\DataCollector; + +/** + * @author Laurent VOULLEMIER + */ +abstract class AbstractDataCollector extends DataCollector implements TemplateAwareDataCollectorInterface +{ + public function getName(): string + { + return static::class; + } + + public function reset(): void + { + $this->data = []; + } + + public static function getTemplate(): ?string + { + return null; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php deleted file mode 100644 index a112f890d7a7a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DataCollector; - -use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector as BaseRequestDataCollector; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1. Use %s instead.', RequestDataCollector::class, BaseRequestDataCollector::class), \E_USER_DEPRECATED); - -/** - * RequestDataCollector. - * - * @author Jules Pietri - * - * @deprecated since Symfony 4.1 - */ -class RequestDataCollector extends BaseRequestDataCollector -{ -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php index 60681f7291f55..ccb61b128627f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php @@ -18,11 +18,11 @@ /** * @author Fabien Potencier * - * @final since Symfony 4.4 + * @final */ class RouterDataCollector extends BaseRouterDataCollector { - public function guessRoute(Request $request, $controller) + public function guessRoute(Request $request, mixed $controller) { if (\is_array($controller)) { $controller = $controller[0]; diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/TemplateAwareDataCollectorInterface.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/TemplateAwareDataCollectorInterface.php new file mode 100644 index 0000000000000..5ef17abc54983 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/TemplateAwareDataCollectorInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DataCollector; + +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + +/** + * @author Laurent VOULLEMIER + */ +interface TemplateAwareDataCollectorInterface extends DataCollectorInterface +{ + public static function getTemplate(): ?string; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/CompatibilityServiceSubscriberInterface.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/CompatibilityServiceSubscriberInterface.php deleted file mode 100644 index 41d4aa81e99c1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/CompatibilityServiceSubscriberInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; - -use Symfony\Component\DependencyInjection\ServiceSubscriberInterface as LegacyServiceSubscriberInterface; -use Symfony\Contracts\Service\ServiceSubscriberInterface; - -if (interface_exists(LegacyServiceSubscriberInterface::class)) { - /** - * @internal - */ - interface CompatibilityServiceSubscriberInterface extends LegacyServiceSubscriberInterface - { - } -} else { - /** - * @internal - */ - interface CompatibilityServiceSubscriberInterface extends ServiceSubscriberInterface - { - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddDebugLogProcessorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddDebugLogProcessorPass.php index dbe88b064bddd..a1bf0a80eadb0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddDebugLogProcessorPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddDebugLogProcessorPass.php @@ -34,9 +34,9 @@ public function process(ContainerBuilder $container) $definition->addMethodCall('pushProcessor', [new Reference('debug.log_processor')]); } - public static function configureLogger($logger) + public static function configureLogger(mixed $logger) { - if (method_exists($logger, 'removeDebugLogger') && \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + if (\is_object($logger) && method_exists($logger, 'removeDebugLogger') && \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { $logger->removeDebugLogger(); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php index 3c44205e3e55b..3e2f2768edc1a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass as SecurityExpressionLanguageProvidersPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -23,36 +22,17 @@ */ class AddExpressionLanguageProvidersPass implements CompilerPassInterface { - private $handleSecurityLanguageProviders; - - public function __construct(bool $handleSecurityLanguageProviders = true) - { - if ($handleSecurityLanguageProviders) { - @trigger_error(sprintf('Registering services tagged "security.expression_language_provider" with "%s" is deprecated since Symfony 4.2, use the "%s" instead.', __CLASS__, SecurityExpressionLanguageProvidersPass::class), \E_USER_DEPRECATED); - } - - $this->handleSecurityLanguageProviders = $handleSecurityLanguageProviders; - } - /** * {@inheritdoc} */ public function process(ContainerBuilder $container) { // routing - if ($container->has('router')) { - $definition = $container->findDefinition('router'); + if ($container->has('router.default')) { + $definition = $container->findDefinition('router.default'); foreach ($container->findTaggedServiceIds('routing.expression_language_provider', true) as $id => $attributes) { $definition->addMethodCall('addExpressionLanguageProvider', [new Reference($id)]); } } - - // security - if ($this->handleSecurityLanguageProviders && $container->has('security.expression_language')) { - $definition = $container->findDefinition('security.expression_language'); - foreach ($container->findTaggedServiceIds('security.expression_language_provider', true) as $id => $attributes) { - $definition->addMethodCall('registerProvider', [new Reference($id)]); - } - } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AssetsContextPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AssetsContextPass.php new file mode 100644 index 0000000000000..3fc79f0ee0d64 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AssetsContextPass.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class AssetsContextPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('assets.context')) { + return; + } + + if (!$container->hasDefinition('router.request_context')) { + $container->setParameter('asset.request_context.base_path', $container->getParameter('asset.request_context.base_path') ?? ''); + $container->setParameter('asset.request_context.secure', $container->getParameter('asset.request_context.secure') ?? false); + + return; + } + + $context = $container->getDefinition('assets.context'); + + if (null === $container->getParameter('asset.request_context.base_path')) { + $context->replaceArgument(1, (new Definition('string'))->setFactory([new Reference('router.request_context'), 'getBaseUrl'])); + } + + if (null === $container->getParameter('asset.request_context.secure')) { + $context->replaceArgument(2, (new Definition('bool'))->setFactory([new Reference('router.request_context'), 'isSecure'])); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php deleted file mode 100644 index 93d13834bd3f6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; - -use Symfony\Component\Cache\DependencyInjection\CacheCollectorPass as BaseCacheCollectorPass; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CacheCollectorPass::class, BaseCacheCollectorPass::class), \E_USER_DEPRECATED); - -/** - * Inject a data collector to all the cache services to be able to get detailed statistics. - * - * @author Tobias Nyholm - * - * @deprecated since Symfony 4.2, use Symfony\Component\Cache\DependencyInjection\CacheCollectorPass instead. - */ -class CacheCollectorPass extends BaseCacheCollectorPass -{ -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php deleted file mode 100644 index 58e23ace112cb..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; - -use Symfony\Component\Cache\DependencyInjection\CachePoolClearerPass as BaseCachePoolClearerPass; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolClearerPass::class, BaseCachePoolClearerPass::class), \E_USER_DEPRECATED); - -/** - * @author Nicolas Grekas - * - * @deprecated since version 4.2, use Symfony\Component\Cache\DependencyInjection\CachePoolClearerPass instead. - */ -class CachePoolClearerPass extends BaseCachePoolClearerPass -{ -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php deleted file mode 100644 index 30be3be53bd2c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; - -use Symfony\Component\Cache\DependencyInjection\CachePoolPass as BaseCachePoolPass; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolPass::class, BaseCachePoolPass::class), \E_USER_DEPRECATED); - -/** - * @author Nicolas Grekas - * - * @deprecated since version 4.2, use Symfony\Component\Cache\DependencyInjection\CachePoolPass instead. - */ -class CachePoolPass extends BaseCachePoolPass -{ -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php deleted file mode 100644 index 22237ec01cc6b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; - -use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass as BaseCachePoolPrunerPass; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolPrunerPass::class, BaseCachePoolPrunerPass::class), \E_USER_DEPRECATED); - -/** - * @author Rob Frawley 2nd - * - * @deprecated since Symfony 4.2, use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass instead. - */ -class CachePoolPrunerPass extends BaseCachePoolPrunerPass -{ -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php index 78140985825cc..7200b12b9ba87 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; +use Symfony\Bundle\FrameworkBundle\DataCollector\TemplateAwareDataCollectorInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; @@ -37,11 +38,14 @@ public function process(ContainerBuilder $container) $priority = $attributes[0]['priority'] ?? 0; $template = null; - if (isset($attributes[0]['template'])) { - if (!isset($attributes[0]['id'])) { + $collectorClass = $container->findDefinition($id)->getClass(); + $isTemplateAware = is_subclass_of($collectorClass, TemplateAwareDataCollectorInterface::class); + if (isset($attributes[0]['template']) || $isTemplateAware) { + $idForTemplate = $attributes[0]['id'] ?? $collectorClass; + if (!$idForTemplate) { throw new InvalidArgumentException(sprintf('Data collector service "%s" must have an id attribute in order to specify a template.', $id)); } - $template = [$attributes[0]['id'], $attributes[0]['template']]; + $template = [$idForTemplate, $attributes[0]['template'] ?? $collectorClass::getTemplate()]; } $collectors->insert([$id, $template], [$priority, --$order]); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/RemoveUnusedSessionMarshallingHandlerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/RemoveUnusedSessionMarshallingHandlerPass.php new file mode 100644 index 0000000000000..8b6479c4f2edd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/RemoveUnusedSessionMarshallingHandlerPass.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Ahmed TAILOULOUTE + */ +class RemoveUnusedSessionMarshallingHandlerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('session.marshalling_handler')) { + return; + } + + $isMarshallerDecorated = false; + + foreach ($container->getDefinitions() as $definition) { + $decorated = $definition->getDecoratedService(); + if (null !== $decorated && 'session.marshaller' === $decorated[0]) { + $isMarshallerDecorated = true; + + break; + } + } + + if (!$isMarshallerDecorated) { + $container->removeDefinition('session.marshalling_handler'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SessionPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SessionPass.php deleted file mode 100644 index 0f4950615fbce..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SessionPass.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @internal to be removed in 6.0 - */ -class SessionPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container) - { - if (!$container->hasDefinition('session')) { - return; - } - - $bags = [ - 'session.flash_bag' => $container->hasDefinition('session.flash_bag') ? $container->getDefinition('session.flash_bag') : null, - 'session.attribute_bag' => $container->hasDefinition('session.attribute_bag') ? $container->getDefinition('session.attribute_bag') : null, - ]; - - foreach ($container->getDefinition('session')->getArguments() as $v) { - if (!$v instanceof Reference || !isset($bags[$bag = (string) $v]) || !\is_array($factory = $bags[$bag]->getFactory())) { - continue; - } - - if ([0, 1] !== array_keys($factory) || !$factory[0] instanceof Reference || 'session' !== (string) $factory[0]) { - continue; - } - - if ('get'.ucfirst(substr($bag, 8, -4)).'Bag' !== $factory[1]) { - continue; - } - - $bags[$bag]->setFactory(null); - } - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php deleted file mode 100644 index cbbce7e31eccf..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; - -use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface as FrameworkBundleEngineInterface; -use Symfony\Component\DependencyInjection\Alias; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\Templating\EngineInterface as ComponentEngineInterface; - -/** - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplatingPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container) - { - if ($container->hasDefinition('templating')) { - return; - } - - if ($container->hasAlias('templating')) { - $container->setAlias(ComponentEngineInterface::class, new Alias('templating', false)); - $container->setAlias(FrameworkBundleEngineInterface::class, new Alias('templating', false)); - } - - if ($container->hasDefinition('templating.engine.php')) { - $refs = []; - $helpers = []; - - foreach ($container->findTaggedServiceIds('templating.helper', true) as $id => $attributes) { - if (!$container->getDefinition($id)->isDeprecated()) { - @trigger_error('The "templating.helper" tag is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - } - - if (isset($attributes[0]['alias'])) { - $helpers[$attributes[0]['alias']] = $id; - $refs[$id] = new Reference($id); - } - } - - if (\count($helpers) > 0) { - $definition = $container->getDefinition('templating.engine.php'); - $definition->addMethodCall('setHelpers', [$helpers]); - - if ($container->hasDefinition('templating.engine.php.helpers_locator')) { - $container->getDefinition('templating.engine.php.helpers_locator')->replaceArgument(0, $refs); - } - } - } - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php index 57aa592a32fa5..a68f94f7b6134 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php @@ -33,7 +33,7 @@ public function process(ContainerBuilder $container) $hasErrors = method_exists(Definition::class, 'hasErrors') ? 'hasErrors' : 'getErrors'; foreach ($definitions as $id => $definition) { - if ($id && '.' !== $id[0] && (!$definition->isPublic() || $definition->isPrivate()) && !$definition->$hasErrors() && !$definition->isAbstract()) { + if ($id && '.' !== $id[0] && (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) && !$definition->$hasErrors() && !$definition->isAbstract()) { $privateServices[$id] = new Reference($id, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 888a5ea8d64c1..a556599e76d0c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -21,23 +21,30 @@ */ class UnusedTagsPass implements CompilerPassInterface { - private $knownTags = [ + private const KNOWN_TAGS = [ 'annotations.cached_reader', + 'assets.package', 'auto_alias', 'cache.pool', 'cache.pool.clearer', + 'chatter.transport_factory', 'config_cache.resource_checker', 'console.command', 'container.env_var_loader', 'container.env_var_processor', 'container.hot_path', + 'container.no_preload', + 'container.preload', + 'container.private', 'container.reversible', 'container.service_locator', 'container.service_locator_context', 'container.service_subscriber', + 'container.stack', 'controller.argument_value_resolver', 'controller.service_arguments', 'data_collector', + 'event_dispatcher.dispatcher', 'form.type', 'form.type_extension', 'form.type_guesser', @@ -49,6 +56,7 @@ class UnusedTagsPass implements CompilerPassInterface 'kernel.fragment_renderer', 'kernel.locale_aware', 'kernel.reset', + 'ldap', 'mailer.transport_factory', 'messenger.bus', 'messenger.message_handler', @@ -56,39 +64,43 @@ class UnusedTagsPass implements CompilerPassInterface 'messenger.transport_factory', 'mime.mime_type_guesser', 'monolog.logger', + 'notifier.channel', 'property_info.access_extractor', 'property_info.initializable_extractor', 'property_info.list_extractor', 'property_info.type_extractor', 'proxy', + 'routing.expression_language_function', 'routing.expression_language_provider', 'routing.loader', 'routing.route_loader', + 'security.authenticator.login_linker', 'security.expression_language_provider', 'security.remember_me_aware', + 'security.remember_me_handler', 'security.voter', 'serializer.encoder', 'serializer.normalizer', - 'templating.helper', + 'texter.transport_factory', 'translation.dumper', 'translation.extractor', 'translation.loader', + 'translation.provider_factory', 'twig.extension', 'twig.loader', 'twig.runtime', 'validator.auto_mapper', 'validator.constraint_validator', 'validator.initializer', - 'workflow.definition', ]; public function process(ContainerBuilder $container) { - $tags = array_unique(array_merge($container->findTags(), $this->knownTags)); + $tags = array_unique(array_merge($container->findTags(), self::KNOWN_TAGS)); foreach ($container->findUnusedTags() as $tag) { // skip known tags - if (\in_array($tag, $this->knownTags)) { + if (\in_array($tag, self::KNOWN_TAGS)) { continue; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 3875db646ff21..ffb9beb9a0774 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -12,14 +12,17 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Doctrine\Common\Annotations\Annotation; -use Doctrine\Common\Cache\Cache; use Doctrine\DBAL\Connection; +use Psr\Log\LogLevel; use Symfony\Bundle\FullStack; use Symfony\Component\Asset\Package; +use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Form\Form; use Symfony\Component\HttpClient\HttpClient; @@ -28,18 +31,23 @@ use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Notifier\Notifier; +use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\Translator; +use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Validator\Validation; use Symfony\Component\WebLink\HttpHeaderSerializer; +use Symfony\Component\Workflow\WorkflowEvents; /** * FrameworkExtension configuration structure. */ class Configuration implements ConfigurationInterface { - private $debug; + private bool $debug; /** * @param bool $debug Whether debugging is enabled or not @@ -51,10 +59,8 @@ public function __construct(bool $debug) /** * Generates the configuration tree builder. - * - * @return TreeBuilder The tree builder */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('framework'); $rootNode = $treeBuilder->getRootNode(); @@ -68,6 +74,7 @@ public function getConfigTreeBuilder() return $v; }) ->end() + ->fixXmlConfig('enabled_locale') ->children() ->scalarNode('secret')->end() ->scalarNode('http_method_override') @@ -77,18 +84,55 @@ public function getConfigTreeBuilder() ->scalarNode('ide')->defaultNull()->end() ->booleanNode('test')->end() ->scalarNode('default_locale')->defaultValue('en')->end() + ->booleanNode('set_locale_from_accept_language') + ->info('Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed).') + ->defaultFalse() + ->end() + ->booleanNode('set_content_language_from_locale') + ->info('Whether to set the Content-Language HTTP header on the Response using the Request locale.') + ->defaultFalse() + ->end() + ->arrayNode('enabled_locales') + ->info('Defines the possible locales for the application. This list is used for generating translations files, but also to restrict which locales are allowed when it is set from Accept-Language header (using "set_locale_from_accept_language").') + ->prototype('scalar')->end() + ->end() ->arrayNode('trusted_hosts') ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() ->end() + ->scalarNode('trusted_proxies')->end() + ->arrayNode('trusted_headers') + ->fixXmlConfig('trusted_header') + ->performNoDeepMerging() + ->defaultValue(['x-forwarded-for', 'x-forwarded-port', 'x-forwarded-proto']) + ->beforeNormalization()->ifString()->then(function ($v) { return $v ? array_map('trim', explode(',', $v)) : []; })->end() + ->enumPrototype() + ->values([ + 'forwarded', + 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix', + ]) + ->end() + ->end() ->scalarNode('error_controller') ->defaultValue('error_controller') ->end() ->end() ; + $willBeAvailable = static function (string $package, string $class, string $parentPackage = null) { + $parentPackages = (array) $parentPackage; + $parentPackages[] = 'symfony/framework-bundle'; + + return ContainerBuilder::willBeAvailable($package, $class, $parentPackages); + }; + + $enableIfStandalone = static function (string $package, string $class) use ($willBeAvailable) { + return !class_exists(FullStack::class) && $willBeAvailable($package, $class) ? 'canBeDisabled' : 'canBeEnabled'; + }; + $this->addCsrfSection($rootNode); - $this->addFormSection($rootNode); + $this->addFormSection($rootNode, $enableIfStandalone); + $this->addHttpCacheSection($rootNode); $this->addEsiSection($rootNode); $this->addSsiSection($rootNode); $this->addFragmentsSection($rootNode); @@ -97,23 +141,26 @@ public function getConfigTreeBuilder() $this->addRouterSection($rootNode); $this->addSessionSection($rootNode); $this->addRequestSection($rootNode); - $this->addTemplatingSection($rootNode); - $this->addAssetsSection($rootNode); - $this->addTranslatorSection($rootNode); - $this->addValidationSection($rootNode); - $this->addAnnotationsSection($rootNode); - $this->addSerializerSection($rootNode); - $this->addPropertyAccessSection($rootNode); - $this->addPropertyInfoSection($rootNode); - $this->addCacheSection($rootNode); + $this->addAssetsSection($rootNode, $enableIfStandalone); + $this->addTranslatorSection($rootNode, $enableIfStandalone); + $this->addValidationSection($rootNode, $enableIfStandalone, $willBeAvailable); + $this->addAnnotationsSection($rootNode, $willBeAvailable); + $this->addSerializerSection($rootNode, $enableIfStandalone, $willBeAvailable); + $this->addPropertyAccessSection($rootNode, $willBeAvailable); + $this->addPropertyInfoSection($rootNode, $enableIfStandalone); + $this->addCacheSection($rootNode, $willBeAvailable); $this->addPhpErrorsSection($rootNode); - $this->addWebLinkSection($rootNode); - $this->addLockSection($rootNode); - $this->addMessengerSection($rootNode); + $this->addExceptionsSection($rootNode); + $this->addWebLinkSection($rootNode, $enableIfStandalone); + $this->addLockSection($rootNode, $enableIfStandalone); + $this->addMessengerSection($rootNode, $enableIfStandalone); $this->addRobotsIndexSection($rootNode); - $this->addHttpClientSection($rootNode); - $this->addMailerSection($rootNode); + $this->addHttpClientSection($rootNode, $enableIfStandalone); + $this->addMailerSection($rootNode, $enableIfStandalone); $this->addSecretsSection($rootNode); + $this->addNotifierSection($rootNode, $enableIfStandalone); + $this->addRateLimiterSection($rootNode, $enableIfStandalone); + $this->addUidSection($rootNode, $enableIfStandalone); return $treeBuilder; } @@ -125,7 +172,7 @@ private function addSecretsSection(ArrayNodeDefinition $rootNode) ->arrayNode('secrets') ->canBeDisabled() ->children() - ->scalarNode('vault_directory')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.environment%')->cannotBeEmpty()->end() + ->scalarNode('vault_directory')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.runtime_environment%')->cannotBeEmpty()->end() ->scalarNode('local_dotenv_file')->defaultValue('%kernel.project_dir%/.env.%kernel.environment%.local')->end() ->scalarNode('decryption_env_var')->defaultValue('base64:default::SYMFONY_DECRYPTION_SECRET')->end() ->end() @@ -152,13 +199,13 @@ private function addCsrfSection(ArrayNodeDefinition $rootNode) ; } - private function addFormSection(ArrayNodeDefinition $rootNode) + private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('form') ->info('form configuration') - ->{!class_exists(FullStack::class) && class_exists(Form::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/form', Form::class)}() ->children() ->arrayNode('csrf_protection') ->treatFalseLike(['enabled' => false]) @@ -170,6 +217,37 @@ private function addFormSection(ArrayNodeDefinition $rootNode) ->scalarNode('field_name')->defaultValue('_token')->end() ->end() ->end() + // to be deprecated in Symfony 6.1 + ->booleanNode('legacy_error_messages')->end() + ->end() + ->end() + ->end() + ; + } + + private function addHttpCacheSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('http_cache') + ->info('HTTP cache configuration') + ->canBeEnabled() + ->fixXmlConfig('private_header') + ->children() + ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() + ->enumNode('trace_level') + ->values(['none', 'short', 'full']) + ->end() + ->scalarNode('trace_header')->end() + ->integerNode('default_ttl')->end() + ->arrayNode('private_headers') + ->performNoDeepMerging() + ->scalarPrototype()->end() + ->end() + ->booleanNode('allow_reload')->end() + ->booleanNode('allow_revalidate')->end() + ->integerNode('stale_while_revalidate')->end() + ->integerNode('stale_if_error')->end() ->end() ->end() ->end() @@ -224,8 +302,9 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode) ->canBeEnabled() ->children() ->booleanNode('collect')->defaultTrue()->end() + ->scalarNode('collect_parameter')->defaultNull()->info('The name of the parameter to use to enable or disable collection on a per request basis')->end() ->booleanNode('only_exceptions')->defaultFalse()->end() - ->booleanNode('only_master_requests')->defaultFalse()->end() + ->booleanNode('only_main_requests')->defaultFalse()->end() ->scalarNode('dsn')->defaultValue('file:%kernel.cache_dir%/profiler')->end() ->end() ->end() @@ -250,7 +329,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) $workflows = []; } - if (1 === \count($workflows) && isset($workflows['workflows']) && array_keys($workflows['workflows']) !== range(0, \count($workflows) - 1) && !empty(array_diff(array_keys($workflows['workflows']), ['audit_trail', 'type', 'marking_store', 'supports', 'support_strategy', 'initial_marking', 'places', 'transitions']))) { + if (1 === \count($workflows) && isset($workflows['workflows']) && !array_is_list($workflows['workflows']) && !empty(array_diff(array_keys($workflows['workflows']), ['audit_trail', 'type', 'marking_store', 'supports', 'support_strategy', 'initial_marking', 'places', 'transitions']))) { $workflows = $workflows['workflows']; } @@ -275,18 +354,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->arrayNode('workflows') ->useAttributeAsKey('name') ->prototype('array') - ->beforeNormalization() - ->always(function ($v) { - if (isset($v['initial_place'])) { - $v['initial_marking'] = [$v['initial_place']]; - } - - return $v; - }) - ->end() ->fixXmlConfig('support') ->fixXmlConfig('place') ->fixXmlConfig('transition') + ->fixXmlConfig('event_to_dispatch', 'events_to_dispatch') ->children() ->arrayNode('audit_trail') ->canBeEnabled() @@ -296,44 +367,17 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->defaultValue('state_machine') ->end() ->arrayNode('marking_store') - ->fixXmlConfig('argument') ->children() ->enumNode('type') - ->values(['multiple_state', 'single_state', 'method']) - ->validate() - ->ifTrue(function ($v) { return 'method' !== $v; }) - ->then(function ($v) { - @trigger_error('Passing something else than "method" has been deprecated in Symfony 4.3.', \E_USER_DEPRECATED); - - return $v; - }) - ->end() - ->end() - ->arrayNode('arguments') - ->setDeprecated('The "%path%.%node%" configuration key has been deprecated in Symfony 4.3. Use "property" instead.') - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return [$v]; }) - ->end() - ->requiresAtLeastOneElement() - ->prototype('scalar') - ->end() + ->values(['method']) ->end() ->scalarNode('property') - ->defaultNull() // In Symfony 5.0, set "marking" as default property + ->defaultValue('marking') ->end() ->scalarNode('service') ->cannotBeEmpty() ->end() ->end() - ->validate() - ->ifTrue(function ($v) { return isset($v['type']) && isset($v['service']); }) - ->thenInvalid('"type" and "service" cannot be used together.') - ->end() - ->validate() - ->ifTrue(function ($v) { return !empty($v['arguments']) && isset($v['service']); }) - ->thenInvalid('"arguments" and "service" cannot be used together.') - ->end() ->end() ->arrayNode('supports') ->beforeNormalization() @@ -351,15 +395,38 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->scalarNode('support_strategy') ->cannotBeEmpty() ->end() - ->scalarNode('initial_place') - ->setDeprecated('The "%path%.%node%" configuration key has been deprecated in Symfony 4.3, use the "initial_marking" configuration key instead.') - ->defaultNull() - ->end() ->arrayNode('initial_marking') ->beforeNormalization()->castToArray()->end() ->defaultValue([]) ->prototype('scalar')->end() ->end() + ->variableNode('events_to_dispatch') + ->defaultValue(null) + ->validate() + ->ifTrue(function ($v) { + if (null === $v) { + return false; + } + if (!\is_array($v)) { + return true; + } + + foreach ($v as $value) { + if (!\is_string($value)) { + return true; + } + if (class_exists(WorkflowEvents::class) && !\in_array($value, WorkflowEvents::ALIASES)) { + return true; + } + } + + return false; + }) + ->thenInvalid('The value must be "null" or an array of workflow events (like ["workflow.enter"]).') + ->end() + ->info('Select which Transition events should be dispatched for this Workflow') + ->example(['workflow.enter', 'workflow.transition']) + ->end() ->arrayNode('places') ->beforeNormalization() ->always() @@ -488,23 +555,17 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) }) ->thenInvalid('"supports" or "support_strategy" should be configured.') ->end() - ->validate() - ->ifTrue(function ($v) { - return 'workflow' === $v['type'] && 'single_state' === ($v['marking_store']['type'] ?? false); - }) - ->then(function ($v) { - @trigger_error('Using a workflow with type=workflow and a marking_store=single_state is deprecated since Symfony 4.3. Use type=state_machine instead.', \E_USER_DEPRECATED); + ->beforeNormalization() + ->always() + ->then(function ($values) { + // Special case to deal with XML when the user wants an empty array + if (\array_key_exists('event_to_dispatch', $values) && null === $values['event_to_dispatch']) { + $values['events_to_dispatch'] = []; + unset($values['event_to_dispatch']); + } - return $v; - }) - ->end() - ->validate() - ->ifTrue(function ($v) { - return isset($v['marking_store']['property']) - && (!isset($v['marking_store']['type']) || 'method' !== $v['marking_store']['type']) - ; - }) - ->thenInvalid('"property" option is only supported by the "method" marking store.') + return $values; + }) ->end() ->end() ->end() @@ -524,6 +585,10 @@ private function addRouterSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('resource')->isRequired()->end() ->scalarNode('type')->end() + ->scalarNode('default_uri') + ->info('The default URI used to generate URLs in a non-HTTP context') + ->defaultNull() + ->end() ->scalarNode('http_port')->defaultValue(80)->end() ->scalarNode('https_port')->defaultValue(443)->end() ->scalarNode('strict_requirements') @@ -535,7 +600,7 @@ private function addRouterSection(ArrayNodeDefinition $rootNode) ) ->defaultTrue() ->end() - ->booleanNode('utf8')->defaultFalse()->end() + ->booleanNode('utf8')->defaultTrue()->end() ->end() ->end() ->end() @@ -550,7 +615,7 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) ->info('session configuration') ->canBeEnabled() ->children() - ->scalarNode('storage_id')->defaultValue('session.storage.native')->end() + ->scalarNode('storage_factory_id')->defaultValue('session.storage.factory.native')->end() ->scalarNode('handler_id')->defaultValue('session.handler.native_file')->end() ->scalarNode('name') ->validate() @@ -617,73 +682,19 @@ private function addRequestSection(ArrayNodeDefinition $rootNode) ; } - private function addTemplatingSection(ArrayNodeDefinition $rootNode) - { - $rootNode - ->children() - ->arrayNode('templating') - ->info('templating configuration') - ->canBeEnabled() - ->setDeprecated('The "%path%.%node%" configuration is deprecated since Symfony 4.3. Configure the "twig" section provided by the Twig Bundle instead.') - ->beforeNormalization() - ->ifTrue(function ($v) { return false === $v || \is_array($v) && false === $v['enabled']; }) - ->then(function () { return ['enabled' => false, 'engines' => false]; }) - ->end() - ->children() - ->scalarNode('hinclude_default_template')->setDeprecated('Setting "templating.hinclude_default_template" is deprecated since Symfony 4.3, use "fragments.hinclude_default_template" instead.')->defaultNull()->end() - ->scalarNode('cache')->end() - ->arrayNode('form') - ->addDefaultsIfNotSet() - ->fixXmlConfig('resource') - ->children() - ->arrayNode('resources') - ->addDefaultChildrenIfNoneSet() - ->prototype('scalar')->defaultValue('FrameworkBundle:Form')->end() - ->validate() - ->ifTrue(function ($v) {return !\in_array('FrameworkBundle:Form', $v); }) - ->then(function ($v) { - return array_merge(['FrameworkBundle:Form'], $v); - }) - ->end() - ->end() - ->end() - ->end() - ->end() - ->fixXmlConfig('engine') - ->children() - ->arrayNode('engines') - ->example(['twig']) - ->isRequired() - ->requiresAtLeastOneElement() - ->canBeUnset() - ->beforeNormalization() - ->ifTrue(function ($v) { return !\is_array($v) && false !== $v; }) - ->then(function ($v) { return [$v]; }) - ->end() - ->prototype('scalar')->end() - ->end() - ->end() - ->fixXmlConfig('loader') - ->children() - ->arrayNode('loaders') - ->beforeNormalization()->castToArray()->end() - ->prototype('scalar')->end() - ->end() - ->end() - ->end() - ->end() - ; - } - - private function addAssetsSection(ArrayNodeDefinition $rootNode) + private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('assets') ->info('assets configuration') - ->{!class_exists(FullStack::class) && class_exists(Package::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/asset', Package::class)}() ->fixXmlConfig('base_url') ->children() + ->booleanNode('strict_mode') + ->info('Throw an exception if an entry is missing from the manifest.json') + ->defaultFalse() + ->end() ->scalarNode('version_strategy')->defaultNull()->end() ->scalarNode('version')->defaultNull()->end() ->scalarNode('version_format')->defaultValue('%%s?%%s')->end() @@ -721,6 +732,10 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode) ->prototype('array') ->fixXmlConfig('base_url') ->children() + ->booleanNode('strict_mode') + ->info('Throw an exception if an entry is missing from the manifest.json') + ->defaultFalse() + ->end() ->scalarNode('version_strategy')->defaultNull()->end() ->scalarNode('version') ->beforeNormalization() @@ -763,15 +778,16 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode) ; } - private function addTranslatorSection(ArrayNodeDefinition $rootNode) + private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('translator') ->info('translator configuration') - ->{!class_exists(FullStack::class) && class_exists(Translator::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/translation', Translator::class)}() ->fixXmlConfig('fallback') ->fixXmlConfig('path') + ->fixXmlConfig('provider') ->children() ->arrayNode('fallbacks') ->info('Defaults to the value of "default_locale".') @@ -789,43 +805,59 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->arrayNode('paths') ->prototype('scalar')->end() ->end() + ->arrayNode('pseudo_localization') + ->canBeEnabled() + ->fixXmlConfig('localizable_html_attribute') + ->children() + ->booleanNode('accents')->defaultTrue()->end() + ->floatNode('expansion_factor') + ->min(1.0) + ->defaultValue(1.0) + ->end() + ->booleanNode('brackets')->defaultTrue()->end() + ->booleanNode('parse_html')->defaultFalse()->end() + ->arrayNode('localizable_html_attributes') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->arrayNode('providers') + ->info('Translation providers you can read/write your translations from') + ->useAttributeAsKey('name') + ->prototype('array') + ->fixXmlConfig('domain') + ->fixXmlConfig('locale') + ->children() + ->scalarNode('dsn')->end() + ->arrayNode('domains') + ->prototype('scalar')->end() + ->defaultValue([]) + ->end() + ->arrayNode('locales') + ->prototype('scalar')->end() + ->defaultValue([]) + ->info('If not set, all locales listed under framework.enabled_locales are used.') + ->end() + ->end() + ->end() + ->defaultValue([]) + ->end() ->end() ->end() ->end() ; } - private function addValidationSection(ArrayNodeDefinition $rootNode) + private function addValidationSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone, callable $willBeAvailable) { $rootNode ->children() ->arrayNode('validation') ->info('validation configuration') - ->{!class_exists(FullStack::class) && class_exists(Validation::class) ? 'canBeDisabled' : 'canBeEnabled'}() - ->validate() - ->ifTrue(function ($v) { return isset($v['strict_email']) && isset($v['email_validation_mode']); }) - ->thenInvalid('"strict_email" and "email_validation_mode" cannot be used together.') - ->end() - ->beforeNormalization() - ->ifTrue(function ($v) { return isset($v['strict_email']); }) - ->then(function ($v) { - @trigger_error('The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead.', \E_USER_DEPRECATED); - - return $v; - }) - ->end() - ->beforeNormalization() - ->ifTrue(function ($v) { return isset($v['strict_email']) && !isset($v['email_validation_mode']); }) - ->then(function ($v) { - $v['email_validation_mode'] = $v['strict_email'] ? 'strict' : 'loose'; - unset($v['strict_email']); - - return $v; - }) - ->end() + ->{$enableIfStandalone('symfony/validator', Validation::class)}() ->children() ->scalarNode('cache')->end() - ->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end() + ->booleanNode('enable_annotations')->{!class_exists(FullStack::class) ? 'defaultTrue' : 'defaultFalse'}()->end() ->arrayNode('static_method') ->defaultValue(['loadValidatorMetadata']) ->prototype('scalar')->end() @@ -833,7 +865,6 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ->validate()->castToArray()->end() ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() - ->booleanNode('strict_email')->end() ->enumNode('email_validation_mode')->values(['html5', 'loose', 'strict'])->end() ->arrayNode('mapping') ->addDefaultsIfNotSet() @@ -907,15 +938,18 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ; } - private function addAnnotationsSection(ArrayNodeDefinition $rootNode) + private function addAnnotationsSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable) { $rootNode ->children() ->arrayNode('annotations') ->info('annotation configuration') - ->{class_exists(Annotation::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$willBeAvailable('doctrine/annotations', Annotation::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->children() - ->scalarNode('cache')->defaultValue(interface_exists(Cache::class) ? 'php_array' : 'none')->end() + ->enumNode('cache') + ->values(['none', 'php_array', 'file']) + ->defaultValue('php_array') + ->end() ->scalarNode('file_cache_dir')->defaultValue('%kernel.cache_dir%/annotations')->end() ->booleanNode('debug')->defaultValue($this->debug)->end() ->end() @@ -924,15 +958,15 @@ private function addAnnotationsSection(ArrayNodeDefinition $rootNode) ; } - private function addSerializerSection(ArrayNodeDefinition $rootNode) + private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone, callable $willBeAvailable) { $rootNode ->children() ->arrayNode('serializer') ->info('serializer configuration') - ->{!class_exists(FullStack::class) && class_exists(Serializer::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/serializer', Serializer::class)}() ->children() - ->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end() + ->booleanNode('enable_annotations')->{!class_exists(FullStack::class) ? 'defaultTrue' : 'defaultFalse'}()->end() ->scalarNode('name_converter')->end() ->scalarNode('circular_reference_handler')->end() ->scalarNode('max_depth_handler')->end() @@ -945,21 +979,30 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() + ->arrayNode('default_context') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->defaultValue([]) + ->prototype('variable')->end() + ->end() ->end() ->end() ->end() ; } - private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) + private function addPropertyAccessSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable) { $rootNode ->children() ->arrayNode('property_access') ->addDefaultsIfNotSet() ->info('Property access configuration') + ->{$willBeAvailable('symfony/property-access', PropertyAccessor::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->children() ->booleanNode('magic_call')->defaultFalse()->end() + ->booleanNode('magic_get')->defaultTrue()->end() + ->booleanNode('magic_set')->defaultTrue()->end() ->booleanNode('throw_exception_on_invalid_index')->defaultFalse()->end() ->booleanNode('throw_exception_on_invalid_property_path')->defaultTrue()->end() ->end() @@ -968,19 +1011,19 @@ private function addPropertyAccessSection(ArrayNodeDefinition $rootNode) ; } - private function addPropertyInfoSection(ArrayNodeDefinition $rootNode) + private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('property_info') ->info('Property info configuration') - ->{!class_exists(FullStack::class) && interface_exists(PropertyInfoExtractorInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/property-info', PropertyInfoExtractorInterface::class)}() ->end() ->end() ; } - private function addCacheSection(ArrayNodeDefinition $rootNode) + private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable) { $rootNode ->children() @@ -991,7 +1034,8 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->children() ->scalarNode('prefix_seed') ->info('Used to namespace cache keys when using several apps with the same shared backend') - ->example('my-application-name') + ->defaultValue('_%kernel.project_dir%.%kernel.container_class%') + ->example('my-application-name/%kernel.environment%') ->end() ->scalarNode('app') ->info('App related cache pools configuration') @@ -1001,12 +1045,12 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->info('System related cache pools configuration') ->defaultValue('cache.adapter.system') ->end() - ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools')->end() - ->scalarNode('default_doctrine_provider')->end() + ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools/app')->end() ->scalarNode('default_psr6_provider')->end() ->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end() ->scalarNode('default_memcached_provider')->defaultValue('memcached://localhost')->end() - ->scalarNode('default_pdo_provider')->defaultValue(class_exists(Connection::class) ? 'database_connection' : null)->end() + ->scalarNode('default_doctrine_dbal_provider')->defaultValue('database_connection')->end() + ->scalarNode('default_pdo_provider')->defaultValue($willBeAvailable('doctrine/dbal', Connection::class) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null)->end() ->arrayNode('pools') ->useAttributeAsKey('name') ->prototype('array') @@ -1046,10 +1090,16 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->end() ->scalarNode('tags')->defaultNull()->end() ->booleanNode('public')->defaultFalse()->end() - ->integerNode('default_lifetime')->end() + ->scalarNode('default_lifetime') + ->info('Default lifetime of the pool') + ->example('"600" for 5 minutes expressed in seconds, "PT5M" for five minutes expressed as ISO 8601 time interval, or "5 minutes" as a date expression') + ->end() ->scalarNode('provider') ->info('Overwrite the setting from the default provider for this adapter.') ->end() + ->scalarNode('early_expiration_message_bus') + ->example('"messenger.default_bus" to send early expiration events to the default Messenger bus.') + ->end() ->scalarNode('clearer')->end() ->end() ->end() @@ -1072,14 +1122,31 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode) ->info('PHP errors handling configuration') ->addDefaultsIfNotSet() ->children() - ->scalarNode('log') + ->variableNode('log') ->info('Use the application logger instead of the PHP logger for logging PHP errors.') - ->example('"true" to use the default configuration: log all errors. "false" to disable. An integer bit field of E_* constants.') + ->example('"true" to use the default configuration: log all errors. "false" to disable. An integer bit field of E_* constants, or an array mapping E_* constants to log levels.') ->defaultValue($this->debug) ->treatNullLike($this->debug) + ->beforeNormalization() + ->ifArray() + ->then(function (array $v): array { + if (!($v[0]['type'] ?? false)) { + return $v; + } + + // Fix XML normalization + + $ret = []; + foreach ($v as ['type' => $type, 'logLevel' => $logLevel]) { + $ret[$type] = $logLevel; + } + + return $ret; + }) + ->end() ->validate() - ->ifTrue(function ($v) { return !(\is_int($v) || \is_bool($v)); }) - ->thenInvalid('The "php_errors.log" parameter should be either an integer or a boolean.') + ->ifTrue(function ($v) { return !(\is_int($v) || \is_bool($v) || \is_array($v)); }) + ->thenInvalid('The "php_errors.log" parameter should be either an integer, a boolean, or an array') ->end() ->end() ->booleanNode('throw') @@ -1093,13 +1160,71 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode) ; } - private function addLockSection(ArrayNodeDefinition $rootNode) + private function addExceptionsSection(ArrayNodeDefinition $rootNode) + { + $logLevels = (new \ReflectionClass(LogLevel::class))->getConstants(); + + $rootNode + ->children() + ->arrayNode('exceptions') + ->info('Exception handling configuration') + ->beforeNormalization() + ->ifArray() + ->then(function (array $v): array { + if (!\array_key_exists('exception', $v)) { + return $v; + } + + // Fix XML normalization + $data = isset($v['exception'][0]) ? $v['exception'] : [$v['exception']]; + $exceptions = []; + foreach ($data as $exception) { + $config = []; + if (\array_key_exists('log-level', $exception)) { + $config['log_level'] = $exception['log-level']; + } + if (\array_key_exists('status-code', $exception)) { + $config['status_code'] = $exception['status-code']; + } + $exceptions[$exception['name']] = $config; + } + + return $exceptions; + }) + ->end() + ->prototype('array') + ->fixXmlConfig('exception') + ->children() + ->scalarNode('log_level') + ->info('The level of log message. Null to let Symfony decide.') + ->validate() + ->ifTrue(function ($v) use ($logLevels) { return !\in_array($v, $logLevels); }) + ->thenInvalid(sprintf('The log level is not valid. Pick one among "%s".', implode('", "', $logLevels))) + ->end() + ->defaultNull() + ->end() + ->scalarNode('status_code') + ->info('The status code of the response. Null to let Symfony decide.') + ->validate() + ->ifTrue(function ($v) { return $v < 100 || $v > 599; }) + ->thenInvalid('The log level is not valid. Pick a value between 100 and 599.') + ->end() + ->defaultNull() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('lock') ->info('Lock configuration') - ->{!class_exists(FullStack::class) && class_exists(Lock::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/lock', Lock::class)}() ->beforeNormalization() ->ifString()->then(function ($v) { return ['enabled' => true, 'resources' => $v]; }) ->end() @@ -1128,19 +1253,17 @@ private function addLockSection(ArrayNodeDefinition $rootNode) ->ifString()->then(function ($v) { return ['default' => $v]; }) ->end() ->beforeNormalization() - ->ifTrue(function ($v) { return \is_array($v) && array_keys($v) === range(0, \count($v) - 1); }) + ->ifTrue(function ($v) { return \is_array($v) && array_is_list($v); }) ->then(function ($v) { $resources = []; foreach ($v as $resource) { - $resources = array_merge_recursive( - $resources, - \is_array($resource) && isset($resource['name']) - ? [$resource['name'] => $resource['value']] - : ['default' => $resource] - ); + $resources[] = \is_array($resource) && isset($resource['name']) + ? [$resource['name'] => $resource['value']] + : ['default' => $resource] + ; } - return $resources; + return array_merge_recursive([], ...$resources); }) ->end() ->prototype('array') @@ -1155,25 +1278,25 @@ private function addLockSection(ArrayNodeDefinition $rootNode) ; } - private function addWebLinkSection(ArrayNodeDefinition $rootNode) + private function addWebLinkSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('web_link') ->info('web links configuration') - ->{!class_exists(FullStack::class) && class_exists(HttpHeaderSerializer::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/weblink', HttpHeaderSerializer::class)}() ->end() ->end() ; } - private function addMessengerSection(ArrayNodeDefinition $rootNode) + private function addMessengerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('messenger') ->info('Messenger configuration') - ->{!class_exists(FullStack::class) && interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/messenger', MessageBusInterface::class)}() ->fixXmlConfig('transport') ->fixXmlConfig('bus', 'buses') ->validate() @@ -1270,6 +1393,10 @@ function ($a) { ->prototype('variable') ->end() ->end() + ->scalarNode('failure_transport') + ->defaultNull() + ->info('Transport name to send failed messages to (after all retries have failed).') + ->end() ->arrayNode('retry_strategy') ->addDefaultsIfNotSet() ->beforeNormalization() @@ -1296,6 +1423,14 @@ function ($a) { ->defaultNull() ->info('Transport name to send failed messages to (after all retries have failed).') ->end() + ->booleanNode('reset_on_message') + ->defaultTrue() + ->info('Reset container services after each message.') + ->validate() + ->ifTrue(static fn ($v) => true !== $v) + ->thenInvalid('The "framework.messenger.reset_on_message" configuration option can be set to "true" only. To prevent services resetting after each message you can set the "--no-reset" option in "messenger:consume" command.') + ->end() + ->end() ->scalarNode('default_bus')->defaultNull()->end() ->arrayNode('buses') ->defaultValue(['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]]) @@ -1368,14 +1503,33 @@ private function addRobotsIndexSection(ArrayNodeDefinition $rootNode) ; } - private function addHttpClientSection(ArrayNodeDefinition $rootNode) + private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('http_client') ->info('HTTP Client configuration') - ->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/http-client', HttpClient::class)}() ->fixXmlConfig('scoped_client') + ->beforeNormalization() + ->always(function ($config) { + if (empty($config['scoped_clients']) || !\is_array($config['default_options']['retry_failed'] ?? null)) { + return $config; + } + + foreach ($config['scoped_clients'] as &$scopedConfig) { + if (!isset($scopedConfig['retry_failed']) || true === $scopedConfig['retry_failed']) { + $scopedConfig['retry_failed'] = $config['default_options']['retry_failed']; + continue; + } + if (\is_array($scopedConfig['retry_failed'])) { + $scopedConfig['retry_failed'] = $scopedConfig['retry_failed'] + $config['default_options']['retry_failed']; + } + } + + return $config; + }) + ->end() ->children() ->integerNode('max_host_connections') ->info('The maximum number of connections to a single host.') @@ -1461,8 +1615,12 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->variableNode('md5')->end() ->end() ->end() + ->append($this->addHttpClientRetrySection()) ->end() ->end() + ->scalarNode('mock_response_factory') + ->info('The id of the service that should generate mock responses. It should be either an invokable or an iterable.') + ->end() ->arrayNode('scoped_clients') ->useAttributeAsKey('name') ->normalizeKeys(false) @@ -1600,6 +1758,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->variableNode('md5')->end() ->end() ->end() + ->append($this->addHttpClientRetrySection()) ->end() ->end() ->end() @@ -1609,19 +1768,92 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ; } - private function addMailerSection(ArrayNodeDefinition $rootNode) + private function addHttpClientRetrySection() + { + $root = new NodeBuilder(); + + return $root + ->arrayNode('retry_failed') + ->fixXmlConfig('http_code') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->beforeNormalization() + ->always(function ($v) { + if (isset($v['retry_strategy']) && (isset($v['http_codes']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']) || isset($v['jitter']))) { + throw new \InvalidArgumentException('The "retry_strategy" option cannot be used along with the "http_codes", "delay", "multiplier", "max_delay" or "jitter" options.'); + } + + return $v; + }) + ->end() + ->children() + ->scalarNode('retry_strategy')->defaultNull()->info('service id to override the retry strategy')->end() + ->arrayNode('http_codes') + ->performNoDeepMerging() + ->beforeNormalization() + ->ifArray() + ->then(static function ($v) { + $list = []; + foreach ($v as $key => $val) { + if (is_numeric($val)) { + $list[] = ['code' => $val]; + } elseif (\is_array($val)) { + if (isset($val['code']) || isset($val['methods'])) { + $list[] = $val; + } else { + $list[] = ['code' => $key, 'methods' => $val]; + } + } elseif (true === $val || null === $val) { + $list[] = ['code' => $key]; + } + } + + return $list; + }) + ->end() + ->useAttributeAsKey('code') + ->arrayPrototype() + ->fixXmlConfig('method') + ->children() + ->integerNode('code')->end() + ->arrayNode('methods') + ->beforeNormalization() + ->ifArray() + ->then(function ($v) { + return array_map('strtoupper', $v); + }) + ->end() + ->prototype('scalar')->end() + ->info('A list of HTTP methods that triggers a retry for this status code. When empty, all methods are retried') + ->end() + ->end() + ->end() + ->info('A list of HTTP status code that triggers a retry') + ->end() + ->integerNode('max_retries')->defaultValue(3)->min(0)->end() + ->integerNode('delay')->defaultValue(1000)->min(0)->info('Time in ms to delay (or the initial value when multiplier is used)')->end() + ->floatNode('multiplier')->defaultValue(2)->min(1)->info('If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries)')->end() + ->integerNode('max_delay')->defaultValue(0)->min(0)->info('Max time in ms that a retry should ever be delayed (0 = infinite)')->end() + ->floatNode('jitter')->defaultValue(0.1)->min(0)->max(1)->info('Randomness in percent (between 0 and 1) to apply to the delay')->end() + ->end() + ; + } + + private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) { $rootNode ->children() ->arrayNode('mailer') ->info('Mailer configuration') - ->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->{$enableIfStandalone('symfony/mailer', Mailer::class)}() ->validate() ->ifTrue(function ($v) { return isset($v['dsn']) && \count($v['transports']); }) ->thenInvalid('"dsn" and "transports" cannot be used together.') ->end() ->fixXmlConfig('transport') + ->fixXmlConfig('header') ->children() + ->scalarNode('message_bus')->defaultNull()->info('The message bus to use. Defaults to the default bus if the Messenger component is installed.')->end() ->scalarNode('dsn')->defaultNull()->end() ->arrayNode('transports') ->useAttributeAsKey('name') @@ -1643,6 +1875,170 @@ private function addMailerSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() + ->arrayNode('headers') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('array') + ->normalizeKeys(false) + ->beforeNormalization() + ->ifTrue(function ($v) { return !\is_array($v) || array_keys($v) !== ['value']; }) + ->then(function ($v) { return ['value' => $v]; }) + ->end() + ->children() + ->variableNode('value')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + private function addNotifierSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) + { + $rootNode + ->children() + ->arrayNode('notifier') + ->info('Notifier configuration') + ->{$enableIfStandalone('symfony/notifier', Notifier::class)}() + ->fixXmlConfig('chatter_transport') + ->children() + ->arrayNode('chatter_transports') + ->useAttributeAsKey('name') + ->prototype('scalar')->end() + ->end() + ->end() + ->fixXmlConfig('texter_transport') + ->children() + ->arrayNode('texter_transports') + ->useAttributeAsKey('name') + ->prototype('scalar')->end() + ->end() + ->end() + ->children() + ->booleanNode('notification_on_failed_messages')->defaultFalse()->end() + ->end() + ->children() + ->arrayNode('channel_policy') + ->useAttributeAsKey('name') + ->prototype('array') + ->beforeNormalization()->ifString()->then(function (string $v) { return [$v]; })->end() + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->fixXmlConfig('admin_recipient') + ->children() + ->arrayNode('admin_recipients') + ->prototype('array') + ->children() + ->scalarNode('email')->cannotBeEmpty()->end() + ->scalarNode('phone')->defaultValue('')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) + { + $rootNode + ->children() + ->arrayNode('rate_limiter') + ->info('Rate limiter configuration') + ->{$enableIfStandalone('symfony/rate-limiter', TokenBucketLimiter::class)}() + ->fixXmlConfig('limiter') + ->beforeNormalization() + ->ifTrue(function ($v) { return \is_array($v) && !isset($v['limiters']) && !isset($v['limiter']); }) + ->then(function (array $v) { + $newV = [ + 'enabled' => $v['enabled'] ?? true, + ]; + unset($v['enabled']); + + $newV['limiters'] = $v; + + return $newV; + }) + ->end() + ->children() + ->arrayNode('limiters') + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('lock_factory') + ->info('The service ID of the lock factory used by this limiter (or null to disable locking)') + ->defaultValue('lock.factory') + ->end() + ->scalarNode('cache_pool') + ->info('The cache pool to use for storing the current limiter state') + ->defaultValue('cache.rate_limiter') + ->end() + ->scalarNode('storage_service') + ->info('The service ID of a custom storage implementation, this precedes any configured "cache_pool"') + ->defaultNull() + ->end() + ->enumNode('policy') + ->info('The algorithm to be used by this limiter') + ->isRequired() + ->values(['fixed_window', 'token_bucket', 'sliding_window', 'no_limit']) + ->end() + ->integerNode('limit') + ->info('The maximum allowed hits in a fixed interval or burst') + ->isRequired() + ->end() + ->scalarNode('interval') + ->info('Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).') + ->end() + ->arrayNode('rate') + ->info('Configures the fill rate if "policy" is set to "token_bucket"') + ->children() + ->scalarNode('interval') + ->info('Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).') + ->end() + ->integerNode('amount')->info('Amount of tokens to add each interval')->defaultValue(1)->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + private function addUidSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone) + { + $rootNode + ->children() + ->arrayNode('uid') + ->info('Uid configuration') + ->{$enableIfStandalone('symfony/uid', UuidFactory::class)}() + ->addDefaultsIfNotSet() + ->children() + ->enumNode('default_uuid_version') + ->defaultValue(6) + ->values([6, 4, 1]) + ->end() + ->enumNode('name_based_uuid_version') + ->defaultValue(5) + ->values([5, 3]) + ->end() + ->scalarNode('name_based_uuid_namespace') + ->cannotBeEmpty() + ->end() + ->enumNode('time_based_uuid_version') + ->defaultValue(6) + ->values([6, 1]) + ->end() + ->scalarNode('time_based_uuid_node') + ->cannotBeEmpty() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 39351b15aca9c..eb5aa9dba0368 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,9 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Doctrine\Common\Annotations\AnnotationRegistry; -use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Http\Client\HttpClient; +use phpDocumentor\Reflection\DocBlockFactoryInterface; +use phpDocumentor\Reflection\Types\ContextFactory; +use PHPStan\PhpDocParser\Parser\PhpDocParser; use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface as PsrContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; @@ -24,9 +26,9 @@ use Symfony\Bridge\Twig\Extension\CsrfExtension; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; -use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher; use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface; use Symfony\Bundle\FullStack; +use Symfony\Bundle\MercureBundle\MercureBundle; use Symfony\Component\Asset\PackageInterface; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\Cache\Adapter\AdapterInterface; @@ -37,6 +39,7 @@ use Symfony\Component\Cache\Marshaller\DefaultMarshaller; use Symfony\Component\Cache\Marshaller\MarshallerInterface; use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Resource\DirectoryResource; @@ -54,82 +57,154 @@ use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\LogicException; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Dotenv\Command\DebugCommand; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Finder\Finder; use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\Form; use Symfony\Component\Form\FormTypeExtensionInterface; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; +use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\Lock\Factory; -use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\LockFactory; -use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\PersistingStoreInterface; use Symfony\Component\Lock\Store\StoreFactory; -use Symfony\Component\Lock\StoreInterface; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mercure\HubRegistry; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; +use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; +use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; +use Symfony\Component\Messenger\Handler\BatchHandlerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory; -use Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory; +use Symfony\Component\Messenger\Middleware\RouterContextMiddleware; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportInterface; +use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; +use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; +use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; +use Symfony\Component\Notifier\Bridge\Expo\ExpoTransportFactory; +use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; +use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; +use Symfony\Component\Notifier\Bridge\GatewayApi\GatewayApiTransportFactory; +use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; +use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory; +use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; +use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory; +use Symfony\Component\Notifier\Bridge\Mailjet\MailjetTransportFactory as MailjetNotifierTransportFactory; +use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; +use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransport; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; +use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; +use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; +use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; +use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory as SendinblueNotifierTransportFactory; +use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; +use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; +use Symfony\Component\Notifier\Bridge\Sms77\Sms77TransportFactory; +use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; +use Symfony\Component\Notifier\Bridge\SmsBiuras\SmsBiurasTransportFactory; +use Symfony\Component\Notifier\Bridge\Smsc\SmscTransportFactory; +use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; +use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; +use Symfony\Component\Notifier\Bridge\Telnyx\TelnyxTransportFactory; +use Symfony\Component\Notifier\Bridge\TurboSms\TurboSmsTransport; +use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; +use Symfony\Component\Notifier\Bridge\Vonage\VonageTransportFactory; +use Symfony\Component\Notifier\Bridge\Yunpian\YunpianTransportFactory; +use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; +use Symfony\Component\Notifier\Notifier; +use Symfony\Component\Notifier\Recipient\Recipient; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper; -use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\RateLimiterFactory; +use Symfony\Component\RateLimiter\Storage\CacheStorage; use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader; use Symfony\Component\Routing\Loader\AnnotationFileLoader; -use Symfony\Component\Routing\Matcher\CompiledUrlMatcher; -use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; -use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; -use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\String\LazyString; +use Symfony\Component\String\Slugger\SluggerInterface; +use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory; +use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory; +use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory; use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand; +use Symfony\Component\Translation\PseudoLocalizationTranslator; use Symfony\Component\Translation\Translator; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Uid\Factory\UuidFactory; +use Symfony\Component\Uid\UuidV4; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\ObjectInitializerInterface; -use Symfony\Component\Validator\Util\LegacyTranslatorProxy; +use Symfony\Component\Validator\Validation; use Symfony\Component\WebLink\HttpHeaderSerializer; use Symfony\Component\Workflow; use Symfony\Component\Workflow\WorkflowInterface; use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\CallbackInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\Service\ResetInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; @@ -141,14 +216,17 @@ */ class FrameworkExtension extends Extension { - private $formConfigEnabled = false; - private $translationConfigEnabled = false; - private $sessionConfigEnabled = false; - private $annotationsConfigEnabled = false; - private $validatorConfigEnabled = false; - private $messengerConfigEnabled = false; - private $mailerConfigEnabled = false; - private $httpClientConfigEnabled = false; + private bool $formConfigEnabled = false; + private bool $translationConfigEnabled = false; + private bool $sessionConfigEnabled = false; + private bool $annotationsConfigEnabled = false; + private bool $validatorConfigEnabled = false; + private bool $messengerConfigEnabled = false; + private bool $mailerConfigEnabled = false; + private bool $httpClientConfigEnabled = false; + private bool $notifierConfigEnabled = false; + private bool $propertyAccessConfigEnabled = false; + private static bool $lockConfigEnabled = false; /** * Responds to the app.config configuration parameter. @@ -157,21 +235,21 @@ class FrameworkExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { - $loader = new XmlFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); - $loader->load('web.xml'); - $loader->load('services.xml'); - $loader->load('fragment_renderer.xml'); - $loader->load('error_renderer.xml'); + $loader->load('web.php'); + $loader->load('services.php'); + $loader->load('fragment_renderer.php'); + $loader->load('error_renderer.php'); - if (interface_exists(PsrEventDispatcherInterface::class)) { + if (ContainerBuilder::willBeAvailable('psr/event-dispatcher', PsrEventDispatcherInterface::class, ['symfony/framework-bundle'])) { $container->setAlias(PsrEventDispatcherInterface::class, 'event_dispatcher'); } $container->registerAliasForArgument('parameter_bag', PsrContainerInterface::class); - if (class_exists(Application::class)) { - $loader->load('console.xml'); + if ($this->hasConsole()) { + $loader->load('console.php'); if (!class_exists(BaseXliffLintCommand::class)) { $container->removeDefinition('console.command.xliff_lint'); @@ -179,10 +257,14 @@ public function load(array $configs, ContainerBuilder $container) if (!class_exists(BaseYamlLintCommand::class)) { $container->removeDefinition('console.command.yaml_lint'); } + + if (!class_exists(DebugCommand::class)) { + $container->removeDefinition('console.command.dotenv_debug'); + } } // Load Cache configuration first as it is used by other components - $loader->load('cache.xml'); + $loader->load('cache.php'); $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); @@ -199,7 +281,25 @@ public function load(array $configs, ContainerBuilder $container) } if (class_exists(Translator::class)) { - $loader->load('identity_translator.xml'); + $loader->load('identity_translator.php'); + } + } + + $container->getDefinition('locale_listener')->replaceArgument(3, $config['set_locale_from_accept_language']); + $container->getDefinition('response_listener')->replaceArgument(1, $config['set_content_language_from_locale']); + + // If the slugger is used but the String component is not available, we should throw an error + if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'])) { + $container->register('slugger', 'stdClass') + ->addError('You cannot use the "slugger" service since the String component is not installed. Try running "composer require symfony/string".'); + } else { + if (!ContainerBuilder::willBeAvailable('symfony/translation', LocaleAwareInterface::class, ['symfony/framework-bundle'])) { + $container->register('slugger', 'stdClass') + ->addError('You cannot use the "slugger" service since the Translation contracts are not installed. Try running "composer require symfony/translation".'); + } + + if (!\extension_loaded('intl') && !\defined('PHPUNIT_COMPOSER_INSTALL')) { + trigger_deprecation('', '', 'Please install the "intl" PHP extension for best performance.'); } } @@ -210,36 +310,101 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('kernel.http_method_override', $config['http_method_override']); $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); $container->setParameter('kernel.default_locale', $config['default_locale']); + $container->setParameter('kernel.enabled_locales', $config['enabled_locales']); $container->setParameter('kernel.error_controller', $config['error_controller']); - if (!$container->hasParameter('debug.file_link_format')) { - if (!$container->hasParameter('templating.helper.code.file_link_format')) { - $links = [ - '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', - ]; - $ide = $config['ide']; - // mark any env vars found in the ide setting as used - $container->resolveEnvPlaceholders($ide); + if (($config['trusted_proxies'] ?? false) && ($config['trusted_headers'] ?? false)) { + $container->setParameter('kernel.trusted_proxies', $config['trusted_proxies']); + $container->setParameter('kernel.trusted_headers', $this->resolveTrustedHeaders($config['trusted_headers'])); + } - $container->setParameter('templating.helper.code.file_link_format', str_replace('%', '%%', ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')) ?: ($links[$ide] ?? $ide)); - } - $container->setParameter('debug.file_link_format', '%templating.helper.code.file_link_format%'); + if (!$container->hasParameter('debug.file_link_format')) { + $container->setParameter('debug.file_link_format', $config['ide']); } if (!empty($config['test'])) { - $loader->load('test.xml'); + $loader->load('test.php'); if (!class_exists(AbstractBrowser::class)) { $container->removeDefinition('test.client'); } } + if ($this->isConfigEnabled($container, $config['request'])) { + $this->registerRequestConfiguration($config['request'], $container, $loader); + } + + if ($this->isConfigEnabled($container, $config['assets'])) { + if (!class_exists(\Symfony\Component\Asset\Package::class)) { + throw new LogicException('Asset support cannot be enabled as the Asset component is not installed. Try running "composer require symfony/asset".'); + } + + $this->registerAssetsConfiguration($config['assets'], $container, $loader); + } + + if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) { + $this->registerHttpClientConfiguration($config['http_client'], $container, $loader, $config['profiler']); + } + + if ($this->mailerConfigEnabled = $this->isConfigEnabled($container, $config['mailer'])) { + $this->registerMailerConfiguration($config['mailer'], $container, $loader); + } + + $propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']); + $this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override']); + $this->registerEsiConfiguration($config['esi'], $container, $loader); + $this->registerSsiConfiguration($config['ssi'], $container, $loader); + $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); + $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale'], $config['enabled_locales']); + $this->registerWorkflowConfiguration($config['workflows'], $container, $loader); + $this->registerDebugConfiguration($config['php_errors'], $container, $loader); + $this->registerRouterConfiguration($config['router'], $container, $loader, $config['enabled_locales']); + $this->registerAnnotationsConfiguration($config['annotations'], $container, $loader); + $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); + $this->registerSecretsConfiguration($config['secrets'], $container, $loader); + + $container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']); + + if ($this->isConfigEnabled($container, $config['serializer'])) { + if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) { + throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".'); + } + + $this->registerSerializerConfiguration($config['serializer'], $container, $loader); + } + + if ($propertyInfoEnabled) { + $this->registerPropertyInfoConfiguration($container, $loader); + } + + if (self::$lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) { + $this->registerLockConfiguration($config['lock'], $container, $loader); + } + + if ($this->isConfigEnabled($container, $config['rate_limiter'])) { + if (!interface_exists(LimiterInterface::class)) { + throw new LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".'); + } + + $this->registerRateLimiterConfiguration($config['rate_limiter'], $container, $loader); + } + + if ($this->isConfigEnabled($container, $config['web_link'])) { + if (!class_exists(HttpHeaderSerializer::class)) { + throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".'); + } + + $loader->load('web_link.php'); + } + + if ($this->isConfigEnabled($container, $config['uid'])) { + if (!class_exists(UuidFactory::class)) { + throw new LogicException('Uid support cannot be enabled as the Uid component is not installed. Try running "composer require symfony/uid".'); + } + + $this->registerUidConfiguration($config['uid'], $container, $loader); + } + // register cache before session so both can share the connection services $this->registerCacheConfiguration($config['cache'], $container); @@ -251,28 +416,30 @@ public function load(array $configs, ContainerBuilder $container) $this->sessionConfigEnabled = true; $this->registerSessionConfiguration($config['session'], $container, $loader); if (!empty($config['test'])) { - $container->getDefinition('test.session.listener')->setArgument(1, '%session.storage.options%'); + // test listener will replace the existing session listener + // as we are aliasing to avoid duplicated registered events + $container->setAlias('session_listener', 'test.session.listener'); } + } elseif (!empty($config['test'])) { + $container->removeDefinition('test.session.listener'); } - if ($this->isConfigEnabled($container, $config['request'])) { - $this->registerRequestConfiguration($config['request'], $container, $loader); - } - + // csrf depends on session being registered if (null === $config['csrf_protection']['enabled']) { - $config['csrf_protection']['enabled'] = $this->sessionConfigEnabled && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class); + $config['csrf_protection']['enabled'] = $this->sessionConfigEnabled && !class_exists(FullStack::class) && ContainerBuilder::willBeAvailable('symfony/security-csrf', CsrfTokenManagerInterface::class, ['symfony/framework-bundle']); } $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); + // form depends on csrf being registered if ($this->isConfigEnabled($container, $config['form'])) { - if (!class_exists(\Symfony\Component\Form\Form::class)) { + if (!class_exists(Form::class)) { throw new LogicException('Form support cannot be enabled as the Form component is not installed. Try running "composer require symfony/form".'); } $this->formConfigEnabled = true; $this->registerFormConfiguration($config, $container, $loader); - if (class_exists(\Symfony\Component\Validator\Validation::class)) { + if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/form'])) { $config['validation']['enabled'] = true; } else { $container->setParameter('validator.translation_domain', 'validators'); @@ -284,24 +451,10 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('console.command.form_debug'); } - if ($this->isConfigEnabled($container, $config['assets'])) { - if (!class_exists(\Symfony\Component\Asset\Package::class)) { - throw new LogicException('Asset support cannot be enabled as the Asset component is not installed. Try running "composer require symfony/asset".'); - } - - $this->registerAssetsConfiguration($config['assets'], $container, $loader); - } - - if ($this->isConfigEnabled($container, $config['templating'])) { - @trigger_error('Enabling the Templating component is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - - if (!class_exists(\Symfony\Component\Templating\PhpEngine::class)) { - throw new LogicException('Templating support cannot be enabled as the Templating component is not installed. Try running "composer require symfony/templating".'); - } - - $this->registerTemplatingConfiguration($config['templating'], $container, $loader); - } + // validation depends on form, annotations being registered + $this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled); + // messenger depends on validation being registered if ($this->messengerConfigEnabled = $this->isConfigEnabled($container, $config['messenger'])) { $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['validation']); } else { @@ -314,62 +467,34 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('console.command.messenger_failed_messages_remove'); $container->removeDefinition('cache.messenger.restart_workers_signal'); - if ($container->hasDefinition('messenger.transport.amqp.factory') && class_exists(AmqpTransportFactory::class)) { - $container->getDefinition('messenger.transport.amqp.factory') - ->addTag('messenger.transport_factory'); + if ($container->hasDefinition('messenger.transport.amqp.factory') && !class_exists(AmqpTransportFactory::class)) { + if (class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class)) { + $container->getDefinition('messenger.transport.amqp.factory') + ->setClass(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class) + ->addTag('messenger.transport_factory'); + } else { + $container->removeDefinition('messenger.transport.amqp.factory'); + } } - if ($container->hasDefinition('messenger.transport.redis.factory') && class_exists(RedisTransportFactory::class)) { - $container->getDefinition('messenger.transport.redis.factory') - ->addTag('messenger.transport_factory'); + if ($container->hasDefinition('messenger.transport.redis.factory') && !class_exists(RedisTransportFactory::class)) { + if (class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class)) { + $container->getDefinition('messenger.transport.redis.factory') + ->setClass(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class) + ->addTag('messenger.transport_factory'); + } else { + $container->removeDefinition('messenger.transport.redis.factory'); + } } } - if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) { - $this->registerHttpClientConfiguration($config['http_client'], $container, $loader, $config['profiler']); + // notifier depends on messenger, mailer being registered + if ($this->notifierConfigEnabled = $this->isConfigEnabled($container, $config['notifier'])) { + $this->registerNotifierConfiguration($config['notifier'], $container, $loader); } - if ($this->mailerConfigEnabled = $this->isConfigEnabled($container, $config['mailer'])) { - $this->registerMailerConfiguration($config['mailer'], $container, $loader); - } - - $propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']); - $this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled); - $this->registerEsiConfiguration($config['esi'], $container, $loader); - $this->registerSsiConfiguration($config['ssi'], $container, $loader); - $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); - $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']); + // profiler depends on form, validation, translation, messenger, mailer, http-client, notifier being registered $this->registerProfilerConfiguration($config['profiler'], $container, $loader); - $this->registerWorkflowConfiguration($config['workflows'], $container, $loader); - $this->registerDebugConfiguration($config['php_errors'], $container, $loader); - $this->registerRouterConfiguration($config['router'], $container, $loader); - $this->registerAnnotationsConfiguration($config['annotations'], $container, $loader); - $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); - $this->registerSecretsConfiguration($config['secrets'], $container, $loader); - - if ($this->isConfigEnabled($container, $config['serializer'])) { - if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) { - throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".'); - } - - $this->registerSerializerConfiguration($config['serializer'], $container, $loader); - } - - if ($propertyInfoEnabled) { - $this->registerPropertyInfoConfiguration($container, $loader); - } - - if ($this->isConfigEnabled($container, $config['lock'])) { - $this->registerLockConfiguration($config['lock'], $container, $loader); - } - - if ($this->isConfigEnabled($container, $config['web_link'])) { - if (!class_exists(HttpHeaderSerializer::class)) { - throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".'); - } - - $loader->load('web_link.xml'); - } $this->addAnnotatedClassesToCompile([ '**\\Controller\\', @@ -379,10 +504,12 @@ public function load(array $configs, ContainerBuilder $container) 'Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController', ]); - if (class_exists(MimeTypes::class)) { - $loader->load('mime_type.xml'); + if (ContainerBuilder::willBeAvailable('symfony/mime', MimeTypes::class, ['symfony/framework-bundle'])) { + $loader->load('mime_type.php'); } + $container->registerForAutoconfiguration(PackageInterface::class) + ->addTag('assets.package'); $container->registerForAutoconfiguration(Command::class) ->addTag('console.command'); $container->registerForAutoconfiguration(ResourceCheckerInterface::class) @@ -391,6 +518,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('container.env_var_loader'); $container->registerForAutoconfiguration(EnvVarProcessorInterface::class) ->addTag('container.env_var_processor'); + $container->registerForAutoconfiguration(CallbackInterface::class) + ->addTag('container.reversible'); $container->registerForAutoconfiguration(ServiceLocator::class) ->addTag('container.service_locator'); $container->registerForAutoconfiguration(ServiceSubscriberInterface::class) @@ -399,8 +528,6 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('controller.argument_value_resolver'); $container->registerForAutoconfiguration(AbstractController::class) ->addTag('controller.service_arguments'); - $container->registerForAutoconfiguration('Symfony\Bundle\FrameworkBundle\Controller\Controller') - ->addTag('controller.service_arguments'); $container->registerForAutoconfiguration(DataCollectorInterface::class) ->addTag('data_collector'); $container->registerForAutoconfiguration(FormTypeInterface::class) @@ -413,6 +540,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('kernel.cache_clearer'); $container->registerForAutoconfiguration(CacheWarmerInterface::class) ->addTag('kernel.cache_warmer'); + $container->registerForAutoconfiguration(EventDispatcherInterface::class) + ->addTag('event_dispatcher.dispatcher'); $container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('kernel.event_subscriber'); $container->registerForAutoconfiguration(LocaleAwareInterface::class) @@ -449,6 +578,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('validator.initializer'); $container->registerForAutoconfiguration(MessageHandlerInterface::class) ->addTag('messenger.message_handler'); + $container->registerForAutoconfiguration(BatchHandlerInterface::class) + ->addTag('messenger.message_handler'); $container->registerForAutoconfiguration(TransportFactoryInterface::class) ->addTag('messenger.transport_factory'); $container->registerForAutoconfiguration(MimeTypeGuesserInterface::class) @@ -456,6 +587,27 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(LoggerAwareInterface::class) ->addMethodCall('setLogger', [new Reference('logger')]); + $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector) { + $tagAttributes = get_object_vars($attribute); + if ($reflector instanceof \ReflectionMethod) { + if (isset($tagAttributes['method'])) { + throw new LogicException(sprintf('AsEventListener attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); + } + $tagAttributes['method'] = $reflector->getName(); + } + $definition->addTag('kernel.event_listener', $tagAttributes); + }); + $container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void { + $definition->addTag('controller.service_arguments'); + }); + $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute): void { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['from_transport'] = $tagAttributes['fromTransport']; + unset($tagAttributes['fromTransport']); + + $definition->addTag('messenger.message_handler', $tagAttributes); + }); + if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers $container->getDefinition('config_cache_factory')->setArguments([]); @@ -472,6 +624,7 @@ public function load(array $configs, ContainerBuilder $container) 'container.service_locator', 'container.service_subscriber', 'kernel.event_subscriber', + 'kernel.event_listener', 'kernel.locale_aware', 'kernel.reset', ]); @@ -480,21 +633,26 @@ public function load(array $configs, ContainerBuilder $container) /** * {@inheritdoc} */ - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface { return new Configuration($container->getParameter('kernel.debug')); } - private function registerFormConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + protected function hasConsole(): bool { - $loader->load('form.xml'); + return class_exists(Application::class); + } + + private function registerFormConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) + { + $loader->load('form.php'); if (null === $config['form']['csrf_protection']['enabled']) { $config['form']['csrf_protection']['enabled'] = $config['csrf_protection']['enabled']; } if ($this->isConfigEnabled($container, $config['form']['csrf_protection'])) { - $loader->load('form_csrf.xml'); + $loader->load('form_csrf.php'); $container->setParameter('form.type_extension.csrf.enabled', true); $container->setParameter('form.type_extension.csrf.field_name', $config['form']['csrf_protection']['field_name']); @@ -502,7 +660,7 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont $container->setParameter('form.type_extension.csrf.enabled', false); } - if (!class_exists(Translator::class)) { + if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) { $container->removeDefinition('form.type_extension.upload.validator'); } if (!method_exists(CachingFactoryDecorator::class, 'reset')) { @@ -512,7 +670,28 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont } } - private function registerEsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride) + { + $options = $config; + unset($options['enabled']); + + if (!$options['private_headers']) { + unset($options['private_headers']); + } + + $container->getDefinition('http_cache') + ->setPublic($config['enabled']) + ->replaceArgument(3, $options); + + if ($httpMethodOverride) { + $container->getDefinition('http_cache') + ->addArgument((new Definition('void')) + ->setFactory([Request::class, 'enableHttpMethodParameterOverride']) + ); + } + } + + private function registerEsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('fragment.renderer.esi'); @@ -520,10 +699,10 @@ private function registerEsiConfiguration(array $config, ContainerBuilder $conta return; } - $loader->load('esi.xml'); + $loader->load('esi.php'); } - private function registerSsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('fragment.renderer.ssi'); @@ -531,27 +710,24 @@ private function registerSsiConfiguration(array $config, ContainerBuilder $conta return; } - $loader->load('ssi.xml'); + $loader->load('ssi.php'); } - private function registerFragmentsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerFragmentsConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('fragment.renderer.hinclude'); return; } - if ($container->hasParameter('fragment.renderer.hinclude.global_template') && '' !== $container->getParameter('fragment.renderer.hinclude.global_template') && null !== $config['hinclude_default_template']) { - throw new \LogicException('You cannot set both "templating.hinclude_default_template" and "fragments.hinclude_default_template", please only use "fragments.hinclude_default_template".'); - } $container->setParameter('fragment.renderer.hinclude.global_template', $config['hinclude_default_template']); - $loader->load('fragment_listener.xml'); + $loader->load('fragment_listener.php'); $container->setParameter('fragment.path', $config['path']); } - private function registerProfilerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerProfilerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { // this is needed for the WebProfiler to work even if the profiler is disabled @@ -560,38 +736,42 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ return; } - $loader->load('profiling.xml'); - $loader->load('collectors.xml'); - $loader->load('cache_debug.xml'); + $loader->load('profiling.php'); + $loader->load('collectors.php'); + $loader->load('cache_debug.php'); if ($this->formConfigEnabled) { - $loader->load('form_debug.xml'); + $loader->load('form_debug.php'); } if ($this->validatorConfigEnabled) { - $loader->load('validator_debug.xml'); + $loader->load('validator_debug.php'); } if ($this->translationConfigEnabled) { - $loader->load('translation_debug.xml'); + $loader->load('translation_debug.php'); $container->getDefinition('translator.data_collector')->setDecoratedService('translator'); } if ($this->messengerConfigEnabled) { - $loader->load('messenger_debug.xml'); + $loader->load('messenger_debug.php'); } if ($this->mailerConfigEnabled) { - $loader->load('mailer_debug.xml'); + $loader->load('mailer_debug.php'); } if ($this->httpClientConfigEnabled) { - $loader->load('http_client_debug.xml'); + $loader->load('http_client_debug.php'); + } + + if ($this->notifierConfigEnabled) { + $loader->load('notifier_debug.php'); } $container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']); - $container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']); + $container->setParameter('profiler_listener.only_main_requests', $config['only_main_requests']); // Choose storage class based on the DSN [$class] = explode(':', $config['dsn'], 2); @@ -604,9 +784,12 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $container->getDefinition('profiler') ->addArgument($config['collect']) ->addTag('kernel.reset', ['method' => 'reset']); + + $container->getDefinition('profiler_listener') + ->addArgument($config['collect_parameter']); } - private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$config['enabled']) { $container->removeDefinition('console.command.workflow_dump'); @@ -618,10 +801,12 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ throw new LogicException('Workflow support cannot be enabled as the Workflow component is not installed. Try running "composer require symfony/workflow".'); } - $loader->load('workflow.xml'); + $loader->load('workflow.php'); $registryDefinition = $container->getDefinition('workflow.registry'); + $workflows = []; + foreach ($config['workflows'] as $name => $workflow) { $type = $workflow['type']; $workflowId = sprintf('%s.%s', $type, $name); @@ -651,7 +836,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ if ('workflow' === $type) { $transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], $transition['from'], $transition['to']]); $transitionDefinition->setPublic(false); - $transitionId = sprintf('%s.transition.%s', $workflowId, $transitionCounter++); + $transitionId = sprintf('.%s.transition.%s', $workflowId, $transitionCounter++); $container->setDefinition($transitionId, $transitionDefinition); $transitions[] = new Reference($transitionId); if (isset($transition['guard'])) { @@ -673,7 +858,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ foreach ($transition['to'] as $to) { $transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], $from, $to]); $transitionDefinition->setPublic(false); - $transitionId = sprintf('%s.transition.%s', $workflowId, $transitionCounter++); + $transitionId = sprintf('.%s.transition.%s', $workflowId, $transitionCounter++); $container->setDefinition($transitionId, $transitionDefinition); $transitions[] = new Reference($transitionId); if (isset($transition['guard'])) { @@ -695,10 +880,11 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } } $metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition); + $container->setDefinition(sprintf('%s.metadata_store', $workflowId), $metadataStoreDefinition); // Create places $places = array_column($workflow['places'], 'name'); - $initialMarking = $workflow['initial_marking'] ?? $workflow['initial_place'] ?? []; + $initialMarking = $workflow['initial_marking'] ?? []; // Create a Definition $definitionDefinition = new Definition(Workflow\Definition::class); @@ -706,25 +892,17 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $definitionDefinition->addArgument($places); $definitionDefinition->addArgument($transitions); $definitionDefinition->addArgument($initialMarking); - $definitionDefinition->addArgument($metadataStoreDefinition); - $definitionDefinition->addTag('workflow.definition', [ - 'name' => $name, - 'type' => $type, - ]); + $definitionDefinition->addArgument(new Reference(sprintf('%s.metadata_store', $workflowId))); + + $workflows[$workflowId] = $definitionDefinition; // Create MarkingStore if (isset($workflow['marking_store']['type'])) { - $markingStoreDefinition = new ChildDefinition('workflow.marking_store.'.$workflow['marking_store']['type']); - if ('method' === $workflow['marking_store']['type']) { - $markingStoreDefinition->setArguments([ - 'state_machine' === $type, //single state - $workflow['marking_store']['property'] ?? 'marking', - ]); - } else { - foreach ($workflow['marking_store']['arguments'] as $argument) { - $markingStoreDefinition->addArgument($argument); - } - } + $markingStoreDefinition = new ChildDefinition('workflow.marking_store.method'); + $markingStoreDefinition->setArguments([ + 'state_machine' === $type, //single state + $workflow['marking_store']['property'], + ]); } elseif (isset($workflow['marking_store']['service'])) { $markingStoreDefinition = new Reference($workflow['marking_store']['service']); } @@ -732,10 +910,9 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Create Workflow $workflowDefinition = new ChildDefinition(sprintf('%s.abstract', $type)); $workflowDefinition->replaceArgument(0, new Reference(sprintf('%s.definition', $workflowId))); - if (isset($markingStoreDefinition)) { - $workflowDefinition->replaceArgument(1, $markingStoreDefinition); - } + $workflowDefinition->replaceArgument(1, $markingStoreDefinition ?? null); $workflowDefinition->replaceArgument(3, $name); + $workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']); // Store to container $container->setDefinition($workflowId, $workflowDefinition); @@ -743,27 +920,18 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type); // Validate Workflow - $validator = null; - switch (true) { - case 'state_machine' === $workflow['type']: - $validator = new Workflow\Validator\StateMachineValidator(); - break; - case 'single_state' === ($workflow['marking_store']['type'] ?? null): - $validator = new Workflow\Validator\WorkflowValidator(true); - break; - case 'multiple_state' === ($workflow['marking_store']['type'] ?? false): - $validator = new Workflow\Validator\WorkflowValidator(false); - break; - } - - if ($validator) { - $trs = array_map(function (Reference $ref) use ($container): Workflow\Transition { - return $container->get((string) $ref); - }, $transitions); - $realDefinition = new Workflow\Definition($places, $trs, $initialMarking); - $validator->validate($realDefinition, $name); + if ('state_machine' === $workflow['type']) { + $validator = new Workflow\Validator\StateMachineValidator(); + } else { + $validator = new Workflow\Validator\WorkflowValidator(); } + $trs = array_map(function (Reference $ref) use ($container): Workflow\Transition { + return $container->get((string) $ref); + }, $transitions); + $realDefinition = new Workflow\Definition($places, $trs, $initialMarking); + $validator->validate($realDefinition, $name); + // Add workflow to Registry if ($workflow['supports']) { foreach ($workflow['supports'] as $supportedClassName) { @@ -778,13 +946,12 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Enable the AuditTrail if ($workflow['audit_trail']['enabled']) { $listener = new Definition(Workflow\EventListener\AuditTrailListener::class); - $listener->setPrivate(true); $listener->addTag('monolog.logger', ['channel' => 'workflow']); $listener->addTag('kernel.event_listener', ['event' => sprintf('workflow.%s.leave', $name), 'method' => 'onLeave']); $listener->addTag('kernel.event_listener', ['event' => sprintf('workflow.%s.transition', $name), 'method' => 'onTransition']); $listener->addTag('kernel.event_listener', ['event' => sprintf('workflow.%s.enter', $name), 'method' => 'onEnter']); $listener->addArgument(new Reference('logger')); - $container->setDefinition(sprintf('%s.listener.audit_trail', $workflowId), $listener); + $container->setDefinition(sprintf('.%s.listener.audit_trail', $workflowId), $listener); } // Add Guard Listener @@ -798,7 +965,6 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } $guard = new Definition(Workflow\EventListener\GuardListener::class); - $guard->setPrivate(true); $guard->setArguments([ $guardsConfiguration, @@ -813,20 +979,22 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $guard->addTag('kernel.event_listener', ['event' => $eventName, 'method' => 'onTransition']); } - $container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard); + $container->setDefinition(sprintf('.%s.listener.guard', $workflowId), $guard); $container->setParameter('workflow.has_guard_listeners', true); } } + + $commandDumpDefinition = $container->getDefinition('console.command.workflow_dump'); + $commandDumpDefinition->setArgument(0, $workflows); } - private function registerDebugConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerDebugConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('debug_prod.xml'); + $loader->load('debug_prod.php'); if (class_exists(Stopwatch::class)) { $container->register('debug.stopwatch', Stopwatch::class) ->addArgument(true) - ->setPrivate(true) ->addTag('kernel.reset', ['method' => 'reset']); $container->setAlias(Stopwatch::class, new Alias('debug.stopwatch', false)); } @@ -834,11 +1002,11 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con $debug = $container->getParameter('kernel.debug'); if ($debug) { - $container->setParameter('debug.container.dump', '%kernel.cache_dir%/%kernel.container_class%.xml'); + $container->setParameter('debug.container.dump', '%kernel.build_dir%/%kernel.container_class%.xml'); } if ($debug && class_exists(Stopwatch::class)) { - $loader->load('debug.xml'); + $loader->load('debug.php'); } $definition = $container->findDefinition('debug.debug_handlers_listener'); @@ -861,48 +1029,58 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con } } - private function registerRouterConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerRouterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $enabledLocales = []) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('console.command.router_debug'); $container->removeDefinition('console.command.router_match'); + $container->removeDefinition('messenger.middleware.router_context'); return; } + if (!class_exists(RouterContextMiddleware::class)) { + $container->removeDefinition('messenger.middleware.router_context'); + } - $loader->load('routing.xml'); + $loader->load('routing.php'); if ($config['utf8']) { - $container->getDefinition('routing.loader')->replaceArgument(2, ['utf8' => true]); + $container->getDefinition('routing.loader')->replaceArgument(1, ['utf8' => true]); + } + + if ($enabledLocales) { + $enabledLocales = implode('|', array_map('preg_quote', $enabledLocales)); + $container->getDefinition('routing.loader')->replaceArgument(2, ['_locale' => $enabledLocales]); + } + + if (!ContainerBuilder::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/framework-bundle', 'symfony/routing'])) { + $container->removeDefinition('router.expression_language_provider'); } $container->setParameter('router.resource', $config['resource']); - $container->setParameter('router.cache_class_prefix', $container->getParameter('kernel.container_class')); // deprecated $router = $container->findDefinition('router.default'); $argument = $router->getArgument(2); $argument['strict_requirements'] = $config['strict_requirements']; if (isset($config['type'])) { $argument['resource_type'] = $config['type']; } - if (!class_exists(CompiledUrlMatcher::class)) { - $argument['matcher_class'] = $argument['matcher_base_class'] = $argument['matcher_base_class'] ?? RedirectableUrlMatcher::class; - $argument['matcher_dumper_class'] = PhpMatcherDumper::class; - $argument['generator_class'] = $argument['generator_base_class'] = $argument['generator_base_class'] ?? UrlGenerator::class; - $argument['generator_dumper_class'] = PhpGeneratorDumper::class; - } $router->replaceArgument(2, $argument); $container->setParameter('request_listener.http_port', $config['http_port']); $container->setParameter('request_listener.https_port', $config['https_port']); - if (!$this->annotationsConfigEnabled) { - return; + if (null !== $config['default_uri']) { + $container->getDefinition('router.request_context') + ->replaceArgument(0, $config['default_uri']); } $container->register('routing.loader.annotation', AnnotatedRouteControllerLoader::class) ->setPublic(false) ->addTag('routing.loader', ['priority' => -10]) - ->addArgument(new Reference('annotation_reader')); + ->setArguments([ + new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE), + '%kernel.environment%', + ]); $container->register('routing.loader.annotation.directory', AnnotationDirectoryLoader::class) ->setPublic(false) @@ -921,12 +1099,13 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co ]); } - private function registerSessionConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSessionConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('session.xml'); + $loader->load('session.php'); // session storage - $container->setAlias('session.storage', $config['storage_id'])->setPrivate(true); + $container->setAlias('session.storage.factory', $config['storage_factory_id']); + $options = ['cache_limiter' => '0']; foreach (['name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'cookie_samesite', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor', 'sid_length', 'sid_bits_per_character'] as $key) { if (isset($config[$key])) { @@ -935,11 +1114,8 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c } if ('auto' === ($options['cookie_secure'] ?? null)) { - $locator = $container->getDefinition('session_listener')->getArgument(0); - $locator->setValues($locator->getValues() + [ - 'session_storage' => new Reference('session.storage', ContainerInterface::IGNORE_ON_INVALID_REFERENCE), - 'request_stack' => new Reference('request_stack'), - ]); + $container->getDefinition('session.storage.factory.native')->replaceArgument(3, true); + $container->getDefinition('session.storage.factory.php_bridge')->replaceArgument(2, true); } $container->setParameter('session.storage.options', $options); @@ -947,9 +1123,10 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c // session handler (the internal callback registered with PHP session management) if (null === $config['handler_id']) { // Set the handler class to be null - $container->getDefinition('session.storage.native')->replaceArgument(1, null); - $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); - $container->setAlias('session.handler', 'session.handler.native_file')->setPrivate(true); + $container->getDefinition('session.storage.factory.native')->replaceArgument(1, null); + $container->getDefinition('session.storage.factory.php_bridge')->replaceArgument(0, null); + + $container->setAlias('session.handler', 'session.handler.native_file'); } else { $container->resolveEnvPlaceholders($config['handler_id'], null, $usedEnvs); @@ -959,9 +1136,9 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->getDefinition('session.abstract_handler') ->replaceArgument(0, $container->hasDefinition($id) ? new Reference($id) : $config['handler_id']); - $container->setAlias('session.handler', 'session.abstract_handler')->setPrivate(true); + $container->setAlias('session.handler', 'session.abstract_handler'); } else { - $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); + $container->setAlias('session.handler', $config['handler_id']); } } @@ -970,109 +1147,29 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c $container->setParameter('session.metadata.update_threshold', $config['metadata_update_threshold']); } - private function registerRequestConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerRequestConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if ($config['formats']) { - $loader->load('request.xml'); + $loader->load('request.php'); $listener = $container->getDefinition('request.add_request_formats_listener'); $listener->replaceArgument(0, $config['formats']); } } - private function registerTemplatingConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerAssetsConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('templating.xml'); - - $container->setParameter('fragment.renderer.hinclude.global_template', $config['hinclude_default_template']); - - if ($container->getParameter('kernel.debug')) { - $logger = new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE); - - $container->getDefinition('templating.loader.cache') - ->addTag('monolog.logger', ['channel' => 'templating']) - ->addMethodCall('setLogger', [$logger]); - $container->getDefinition('templating.loader.chain') - ->addTag('monolog.logger', ['channel' => 'templating']) - ->addMethodCall('setLogger', [$logger]); - } - - if (!empty($config['loaders'])) { - $loaders = array_map(function ($loader) { return new Reference($loader); }, $config['loaders']); - - // Use a delegation unless only a single loader was registered - if (1 === \count($loaders)) { - $container->setAlias('templating.loader', (string) reset($loaders))->setPrivate(true); - } else { - $container->getDefinition('templating.loader.chain')->addArgument($loaders); - $container->setAlias('templating.loader', 'templating.loader.chain')->setPrivate(true); - } - } - - $container->setParameter('templating.loader.cache.path', null); - if (isset($config['cache'])) { - // Wrap the existing loader with cache (must happen after loaders are registered) - $container->setDefinition('templating.loader.wrapped', $container->findDefinition('templating.loader')); - $loaderCache = $container->getDefinition('templating.loader.cache'); - $container->setParameter('templating.loader.cache.path', $config['cache']); - - $container->setDefinition('templating.loader', $loaderCache); - } - - $container->setParameter('templating.engines', $config['engines']); - $engines = array_map(function ($engine) { return new Reference('templating.engine.'.$engine); }, $config['engines']); - - // Use a delegation unless only a single engine was registered - if (1 === \count($engines)) { - $container->setAlias('templating', (string) reset($engines))->setPublic(true); - } else { - $templateEngineDefinition = $container->getDefinition('templating.engine.delegating'); - foreach ($engines as $engine) { - $templateEngineDefinition->addMethodCall('addEngine', [$engine]); - } - $container->setAlias('templating', 'templating.engine.delegating')->setPublic(true); - } - - $container->getDefinition('fragment.renderer.hinclude') - ->addTag('kernel.fragment_renderer', ['alias' => 'hinclude']) - ->replaceArgument(0, new Reference('templating')) - ; - - // configure the PHP engine if needed - if (\in_array('php', $config['engines'], true)) { - $loader->load('templating_php.xml'); - - $container->setParameter('templating.helper.form.resources', $config['form']['resources']); - - if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class)) { - $loader->load('templating_debug.xml'); - - $container->setDefinition('templating.engine.php', $container->findDefinition('debug.templating.engine.php')); - $container->setAlias('debug.templating.engine.php', 'templating.engine.php')->setPrivate(true); - } - - if ($container->has('assets.packages')) { - $container->getDefinition('templating.helper.assets')->replaceArgument(0, new Reference('assets.packages')); - } else { - $container->removeDefinition('templating.helper.assets'); - } - } - } - - private function registerAssetsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) - { - $loader->load('assets.xml'); + $loader->load('assets.php'); if ($config['version_strategy']) { $defaultVersion = new Reference($config['version_strategy']); } else { - $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], $config['json_manifest_path'], '_default'); + $defaultVersion = $this->createVersion($container, $config['version'], $config['version_format'], $config['json_manifest_path'], '_default', $config['strict_mode']); } $defaultPackage = $this->createPackageDefinition($config['base_path'], $config['base_urls'], $defaultVersion); $container->setDefinition('assets._default_package', $defaultPackage); - $namedPackages = []; foreach ($config['packages'] as $name => $package) { if (null !== $package['version_strategy']) { $version = new Reference($package['version_strategy']); @@ -1083,18 +1180,14 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co // let format fallback to main version_format $format = $package['version_format'] ?: $config['version_format']; $version = $package['version'] ?? null; - $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name); + $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name, $package['strict_mode']); } - $container->setDefinition('assets._package_'.$name, $this->createPackageDefinition($package['base_path'], $package['base_urls'], $version)); + $packageDefinition = $this->createPackageDefinition($package['base_path'], $package['base_urls'], $version) + ->addTag('assets.package', ['package' => $name]); + $container->setDefinition('assets._package_'.$name, $packageDefinition); $container->registerAliasForArgument('assets._package_'.$name, PackageInterface::class, $name.'.package'); - $namedPackages[$name] = new Reference('assets._package_'.$name); } - - $container->getDefinition('assets.packages') - ->replaceArgument(0, new Reference('assets._default_package')) - ->replaceArgument(1, $namedPackages) - ; } /** @@ -1116,7 +1209,7 @@ private function createPackageDefinition(?string $basePath, array $baseUrls, Ref return $package; } - private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name): Reference + private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name, bool $strictMode): Reference { // Configuration prevents $version and $jsonManifestPath from being set if (null !== $version) { @@ -1133,6 +1226,7 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s if (null !== $jsonManifestPath) { $def = new ChildDefinition('assets.json_manifest_version_strategy'); $def->replaceArgument(0, $jsonManifestPath); + $def->replaceArgument(2, $strictMode); $container->setDefinition('assets._version_'.$name, $def); return new Reference('assets._version_'.$name); @@ -1141,16 +1235,19 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s return new Reference('assets.empty_version_strategy'); } - private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale) + private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale, array $enabledLocales) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('console.command.translation_debug'); - $container->removeDefinition('console.command.translation_update'); + $container->removeDefinition('console.command.translation_extract'); + $container->removeDefinition('console.command.translation_pull'); + $container->removeDefinition('console.command.translation_push'); return; } - $loader->load('translation.xml'); + $loader->load('translation.php'); + $loader->load('translation_providers.php'); // Use the "real" translator instead of the identity default $container->setAlias('translator', 'translator.default')->setPublic(true); @@ -1161,6 +1258,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $defaultOptions = $translator->getArgument(4); $defaultOptions['cache_dir'] = $config['cache_dir']; $translator->setArgument(4, $defaultOptions); + $translator->setArgument(5, $enabledLocales); $container->setParameter('translator.logging', $config['logging']); $container->setParameter('translator.default_path', $config['default_path']); @@ -1169,35 +1267,28 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $dirs = []; $transPaths = []; $nonExistingDirs = []; - if (class_exists(\Symfony\Component\Validator\Validation::class)) { - $r = new \ReflectionClass(\Symfony\Component\Validator\Validation::class); + if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/translation'])) { + $r = new \ReflectionClass(Validation::class); $dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations'; } - if (class_exists(\Symfony\Component\Form\Form::class)) { - $r = new \ReflectionClass(\Symfony\Component\Form\Form::class); + if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/translation'])) { + $r = new \ReflectionClass(Form::class); $dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations'; } - if (class_exists(\Symfony\Component\Security\Core\Exception\AuthenticationException::class)) { - $r = new \ReflectionClass(\Symfony\Component\Security\Core\Exception\AuthenticationException::class); + if (ContainerBuilder::willBeAvailable('symfony/security-core', AuthenticationException::class, ['symfony/framework-bundle', 'symfony/translation'])) { + $r = new \ReflectionClass(AuthenticationException::class); $dirs[] = $transPaths[] = \dirname($r->getFileName(), 2).'/Resources/translations'; } $defaultDir = $container->getParameterBag()->resolveValue($config['default_path']); - $rootDir = $container->getParameter('kernel.root_dir'); foreach ($container->getParameter('kernel.bundles_metadata') as $name => $bundle) { if ($container->fileExists($dir = $bundle['path'].'/Resources/translations') || $container->fileExists($dir = $bundle['path'].'/translations')) { $dirs[] = $dir; } else { $nonExistingDirs[] = $dir; } - if ($container->fileExists($dir = $rootDir.sprintf('/Resources/%s/translations', $name))) { - @trigger_error(sprintf('Translations directory "%s" is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultDir), \E_USER_DEPRECATED); - $dirs[] = $dir; - } else { - $nonExistingDirs[] = $dir; - } } foreach ($config['paths'] as $dir) { @@ -1212,8 +1303,8 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->getDefinition('console.command.translation_debug')->replaceArgument(5, $transPaths); } - if ($container->hasDefinition('console.command.translation_update')) { - $container->getDefinition('console.command.translation_update')->replaceArgument(6, $transPaths); + if ($container->hasDefinition('console.command.translation_extract')) { + $container->getDefinition('console.command.translation_extract')->replaceArgument(6, $transPaths); } if (null === $defaultDir) { @@ -1224,16 +1315,6 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $nonExistingDirs[] = $defaultDir; } - if ($container->fileExists($dir = $rootDir.'/Resources/translations')) { - if ($dir !== $defaultDir) { - @trigger_error(sprintf('Translations directory "%s" is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultDir), \E_USER_DEPRECATED); - } - - $dirs[] = $dir; - } else { - $nonExistingDirs[] = $dir; - } - // Register translation resources if ($dirs) { $files = []; @@ -1276,15 +1357,76 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $translator->replaceArgument(4, $options); } + + if ($config['pseudo_localization']['enabled']) { + $options = $config['pseudo_localization']; + unset($options['enabled']); + + $container + ->register('translator.pseudo', PseudoLocalizationTranslator::class) + ->setDecoratedService('translator', null, -1) // Lower priority than "translator.data_collector" + ->setArguments([ + new Reference('translator.pseudo.inner'), + $options, + ]); + } + + $classToServices = [ + CrowdinProviderFactory::class => 'translation.provider_factory.crowdin', + LocoProviderFactory::class => 'translation.provider_factory.loco', + LokaliseProviderFactory::class => 'translation.provider_factory.lokalise', + ]; + + $parentPackages = ['symfony/framework-bundle', 'symfony/translation', 'symfony/http-client']; + + foreach ($classToServices as $class => $service) { + $package = substr($service, \strlen('translation.provider_factory.')); + + if (!$container->hasDefinition('http_client') || !ContainerBuilder::willBeAvailable(sprintf('symfony/%s-translation-provider', $package), $class, $parentPackages)) { + $container->removeDefinition($service); + } + } + + if (!$config['providers']) { + return; + } + + $locales = $enabledLocales; + + foreach ($config['providers'] as $provider) { + if ($provider['locales']) { + $locales += $provider['locales']; + } + } + + $locales = array_unique($locales); + + $container->getDefinition('console.command.translation_pull') + ->replaceArgument(4, array_merge($transPaths, [$config['default_path']])) + ->replaceArgument(5, $locales) + ; + + $container->getDefinition('console.command.translation_push') + ->replaceArgument(2, array_merge($transPaths, [$config['default_path']])) + ->replaceArgument(3, $locales) + ; + + $container->getDefinition('translation.provider_collection_factory') + ->replaceArgument(1, $locales) + ; + + $container->getDefinition('translation.provider_collection')->setArgument(0, $config['providers']); } - private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, bool $propertyInfoEnabled) + private function registerValidationConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $propertyInfoEnabled) { if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) { + $container->removeDefinition('console.command.validator_debug'); + return; } - if (!class_exists(\Symfony\Component\Validator\Validation::class)) { + if (!class_exists(Validation::class)) { throw new LogicException('Validation support cannot be enabled as the Validator component is not installed. Try running "composer require symfony/validator".'); } @@ -1292,16 +1434,10 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $config['email_validation_mode'] = 'loose'; } - $loader->load('validator.xml'); + $loader->load('validator.php'); $validatorBuilder = $container->getDefinition('validator.builder'); - if (interface_exists(TranslatorInterface::class) && class_exists(LegacyTranslatorProxy::class)) { - $calls = $validatorBuilder->getMethodCalls(); - $calls[1] = ['setTranslator', [new Definition(LegacyTranslatorProxy::class, [new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])]]; - $validatorBuilder->setMethodCalls($calls); - } - $container->setParameter('validator.translation_domain', $config['translation_domain']); $files = ['xml' => [], 'yml' => []]; @@ -1319,11 +1455,10 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $definition->replaceArgument(0, $config['email_validation_mode']); if (\array_key_exists('enable_annotations', $config) && $config['enable_annotations']) { - if (!$this->annotationsConfigEnabled) { - throw new \LogicException('"enable_annotations" on the validator cannot be set as Annotations support is disabled.'); + $validatorBuilder->addMethodCall('enableAnnotationMapping', [true]); + if ($this->annotationsConfigEnabled) { + $validatorBuilder->addMethodCall('setDoctrineAnnotationReader', [new Reference('annotation_reader')]); } - - $validatorBuilder->addMethodCall('enableAnnotationMapping', [new Reference('annotation_reader')]); } if (\array_key_exists('static_method', $config) && $config['static_method']) { @@ -1346,6 +1481,10 @@ private function registerValidationConfiguration(array $config, ContainerBuilder ->setArgument(2, $config['not_compromised_password']['enabled']) ->setArgument(3, $config['not_compromised_password']['endpoint']) ; + + if (!class_exists(ExpressionLanguage::class)) { + $container->removeDefinition('validator.expression_language'); + } } private function registerValidatorMapping(ContainerBuilder $container, array $config, array &$files) @@ -1354,8 +1493,8 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co $files['yaml' === $extension ? 'yml' : $extension][] = $path; }; - if (interface_exists(\Symfony\Component\Form\FormInterface::class)) { - $reflClass = new \ReflectionClass(\Symfony\Component\Form\FormInterface::class); + if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) { + $reflClass = new \ReflectionClass(Form::class); $fileRecorder('xml', \dirname($reflClass->getFileName()).'/Resources/config/validation.xml'); } @@ -1417,83 +1556,83 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde } if (!class_exists(\Doctrine\Common\Annotations\Annotation::class)) { - throw new LogicException('Annotations cannot be enabled as the Doctrine Annotation library is not installed.'); + throw new LogicException('Annotations cannot be enabled as the Doctrine Annotation library is not installed. Try running "composer require doctrine/annotations".'); } - $loader->load('annotations.xml'); + $loader->load('annotations.php'); if (!method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { $container->getDefinition('annotations.dummy_registry') ->setMethodCalls([['registerLoader', ['class_exists']]]); } - if ('none' !== $config['cache']) { - if (class_exists(PsrCachedReader::class)) { - $container - ->getDefinition('annotations.cached_reader') - ->setClass(PsrCachedReader::class) - ->replaceArgument(1, new Definition(ArrayAdapter::class)) - ; - } elseif (!class_exists(\Doctrine\Common\Cache\CacheProvider::class)) { - throw new LogicException('Annotations cannot be enabled as the Doctrine Cache library is not installed.'); - } - - $cacheService = $config['cache']; - - if ('php_array' === $config['cache']) { - $cacheService = class_exists(PsrCachedReader::class) ? 'annotations.cache_adapter' : 'annotations.cache'; + if ('none' === $config['cache']) { + $container->removeDefinition('annotations.cached_reader'); - // Enable warmer only if PHP array is used for cache - $definition = $container->findDefinition('annotations.cache_warmer'); - $definition->addTag('kernel.cache_warmer'); - } elseif ('file' === $config['cache']) { - $cacheDir = $container->getParameterBag()->resolveValue($config['file_cache_dir']); + return; + } - if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) { - throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); - } + if ('php_array' === $config['cache']) { + $cacheService = 'annotations.cache_adapter'; - $container - ->getDefinition('annotations.filesystem_cache_adapter') - ->replaceArgument(2, $cacheDir) - ; + // Enable warmer only if PHP array is used for cache + $definition = $container->findDefinition('annotations.cache_warmer'); + $definition->addTag('kernel.cache_warmer'); + } else { + $cacheService = 'annotations.filesystem_cache_adapter'; + $cacheDir = $container->getParameterBag()->resolveValue($config['file_cache_dir']); - $cacheService = class_exists(PsrCachedReader::class) ? 'annotations.filesystem_cache_adapter' : 'annotations.filesystem_cache'; + if (!is_dir($cacheDir) && false === @mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) { + throw new \RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); } $container - ->getDefinition('annotations.cached_reader') - ->setPublic(true) // set to false in AddAnnotationsCachedReaderPass - ->replaceArgument(2, $config['debug']) - // reference the cache provider without using it until AddAnnotationsCachedReaderPass runs - ->addArgument(new ServiceClosureArgument(new Reference($cacheService))) - ->addTag('annotations.cached_reader') + ->getDefinition('annotations.filesystem_cache_adapter') + ->replaceArgument(2, $cacheDir) ; - - $container->setAlias('annotation_reader', 'annotations.cached_reader')->setPrivate(true); - $container->setAlias(Reader::class, new Alias('annotations.cached_reader', false)); - } else { - $container->removeDefinition('annotations.cached_reader'); } + + $container + ->getDefinition('annotations.cached_reader') + ->setPublic(true) // set to false in AddAnnotationsCachedReaderPass + ->replaceArgument(2, $config['debug']) + // reference the cache provider without using it until AddAnnotationsCachedReaderPass runs + ->addArgument(new ServiceClosureArgument(new Reference($cacheService))) + ->addTag('annotations.cached_reader') + ; + + $container->setAlias('annotation_reader', 'annotations.cached_reader'); + $container->setAlias(Reader::class, new Alias('annotations.cached_reader', false)); + $container->removeDefinition('annotations.psr_cached_reader'); } - private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - if (!class_exists(PropertyAccessor::class)) { + if (!$this->propertyAccessConfigEnabled = $this->isConfigEnabled($container, $config)) { return; } - $loader->load('property_access.xml'); + $loader->load('property_access.php'); + + $magicMethods = PropertyAccessor::DISALLOW_MAGIC_METHODS; + $magicMethods |= $config['magic_call'] ? PropertyAccessor::MAGIC_CALL : 0; + $magicMethods |= $config['magic_get'] ? PropertyAccessor::MAGIC_GET : 0; + $magicMethods |= $config['magic_set'] ? PropertyAccessor::MAGIC_SET : 0; + + $throw = PropertyAccessor::DO_NOT_THROW; + $throw |= $config['throw_exception_on_invalid_index'] ? PropertyAccessor::THROW_ON_INVALID_INDEX : 0; + $throw |= $config['throw_exception_on_invalid_property_path'] ? PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH : 0; $container ->getDefinition('property_accessor') - ->replaceArgument(0, $config['magic_call']) - ->replaceArgument(1, $config['throw_exception_on_invalid_index']) - ->replaceArgument(3, $config['throw_exception_on_invalid_property_path']) + ->replaceArgument(0, $magicMethods) + ->replaceArgument(1, $throw) + ->replaceArgument(3, new Reference(PropertyReadInfoExtractorInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->replaceArgument(4, new Reference(PropertyWriteInfoExtractorInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) ; } - private function registerSecretsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSecretsConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { $container->removeDefinition('console.command.secrets_set'); @@ -1506,7 +1645,7 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c return; } - $loader->load('secrets.xml'); + $loader->load('secrets.php'); $container->getDefinition('secrets.vault')->replaceArgument(0, $config['vault_directory']); @@ -1517,17 +1656,23 @@ private function registerSecretsConfiguration(array $config, ContainerBuilder $c } if ($config['decryption_env_var']) { - if (!preg_match('/^(?:\w*+:)*+\w++$/', $config['decryption_env_var'])) { + if (!preg_match('/^(?:[-.\w]*+:)*+\w++$/', $config['decryption_env_var'])) { throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var'])); } - $container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%"); + if (ContainerBuilder::willBeAvailable('symfony/string', LazyString::class, ['symfony/framework-bundle'])) { + $container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']); + } else { + $container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%"); + $container->removeDefinition('secrets.decryption_key'); + } } else { $container->getDefinition('secrets.vault')->replaceArgument(1, null); + $container->removeDefinition('secrets.decryption_key'); } } - private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { return; @@ -1542,29 +1687,23 @@ private function registerSecurityCsrfConfiguration(array $config, ContainerBuild } // Enable services for CSRF protection (even without forms) - $loader->load('security_csrf.xml'); + $loader->load('security_csrf.php'); if (!class_exists(CsrfExtension::class)) { $container->removeDefinition('twig.extension.security_csrf'); } } - private function registerSerializerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerSerializerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('serializer.xml'); - - if (!class_exists(ConstraintViolationListNormalizer::class)) { - $container->removeDefinition('serializer.normalizer.constraint_violation_list'); - } - - if (!class_exists(ClassDiscriminatorFromClassMetadata::class)) { - $container->removeAlias('Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface'); - $container->removeDefinition('serializer.mapping.class_discriminator_resolver'); + $loader->load('serializer.php'); + if ($container->getParameter('kernel.debug')) { + $container->removeDefinition('serializer.mapping.cache_class_metadata_factory'); } $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); - if (!class_exists(PropertyAccessor::class)) { + if (!$this->propertyAccessConfigEnabled) { $container->removeAlias('serializer.property_accessor'); $container->removeDefinition('serializer.normalizer.object'); } @@ -1573,15 +1712,19 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.encoder.yaml'); } + if (!class_exists(UnwrappingDenormalizer::class) || !$this->propertyAccessConfigEnabled) { + $container->removeDefinition('serializer.denormalizer.unwrapping'); + } + + if (!class_exists(Headers::class)) { + $container->removeDefinition('serializer.normalizer.mime_message'); + } + $serializerLoaders = []; if (isset($config['enable_annotations']) && $config['enable_annotations']) { - if (!$this->annotationsConfigEnabled) { - throw new \LogicException('"enable_annotations" on the serializer cannot be set as Annotations support is disabled.'); - } - $annotationLoader = new Definition( 'Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader', - [new Reference('annotation_reader')] + [new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE)] ); $annotationLoader->setPublic(false); @@ -1623,10 +1766,6 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $chainLoader->replaceArgument(0, $serializerLoaders); $container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders); - if ($container->getParameter('kernel.debug')) { - $container->removeDefinition('serializer.mapping.cache_class_metadata_factory'); - } - if (isset($config['name_converter']) && $config['name_converter']) { $container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($config['name_converter'])); } @@ -1643,19 +1782,30 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $defaultContext += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; $container->getDefinition('serializer.normalizer.object')->replaceArgument(6, $defaultContext); } + + if (isset($config['default_context']) && $config['default_context']) { + $container->setParameter('serializer.default_context', $config['default_context']); + } } - private function registerPropertyInfoConfiguration(ContainerBuilder $container, XmlFileLoader $loader) + private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader) { if (!interface_exists(PropertyInfoExtractorInterface::class)) { throw new LogicException('PropertyInfo support cannot be enabled as the PropertyInfo component is not installed. Try running "composer require symfony/property-info".'); } - $loader->load('property_info.xml'); + $loader->load('property_info.php'); + + if ( + ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/property-info']) + && ContainerBuilder::willBeAvailable('phpdocumentor/type-resolver', ContextFactory::class, ['symfony/framework-bundle', 'symfony/property-info']) + ) { + $definition = $container->register('property_info.phpstan_extractor', PhpStanExtractor::class); + $definition->addTag('property_info.type_extractor', ['priority' => -1000]); + } - if (interface_exists(\phpDocumentor\Reflection\DocBlockFactoryInterface::class)) { + if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'], true)) { $definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor'); - $definition->setPrivate(true); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]); } @@ -1665,9 +1815,9 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, } } - private function registerLockConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerLockConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { - $loader->load('lock.xml'); + $loader->load('lock.php'); foreach ($config['resources'] as $resourceName => $resourceStores) { if (0 === \count($resourceStores)) { @@ -1678,7 +1828,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont $storeDefinitions = []; foreach ($resourceStores as $storeDsn) { $storeDsn = $container->resolveEnvPlaceholders($storeDsn, null, $usedEnvs); - $storeDefinition = new Definition(interface_exists(StoreInterface::class) ? StoreInterface::class : PersistingStoreInterface::class); + $storeDefinition = new Definition(PersistingStoreInterface::class); $storeDefinition->setFactory([StoreFactory::class, 'createStore']); $storeDefinition->setArguments([$storeDsn]); @@ -1693,59 +1843,52 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont if (\count($storeDefinitions) > 1) { $combinedDefinition = new ChildDefinition('lock.store.combined.abstract'); $combinedDefinition->replaceArgument(0, $storeDefinitions); - $container->setDefinition('lock.'.$resourceName.'.store', $combinedDefinition); - } else { - $container->setAlias('lock.'.$resourceName.'.store', new Alias((string) $storeDefinitions[0], false)); + $container->setDefinition($storeDefinitionId = '.lock.'.$resourceName.'.store.'.$container->hash($resourceStores), $combinedDefinition); } // Generate factories for each resource $factoryDefinition = new ChildDefinition('lock.factory.abstract'); - $factoryDefinition->replaceArgument(0, new Reference('lock.'.$resourceName.'.store')); + $factoryDefinition->replaceArgument(0, new Reference($storeDefinitionId)); $container->setDefinition('lock.'.$resourceName.'.factory', $factoryDefinition); - // Generate services for lock instances - $lockDefinition = new Definition(Lock::class); - $lockDefinition->setPublic(false); - $lockDefinition->setFactory([new Reference('lock.'.$resourceName.'.factory'), 'createLock']); - $lockDefinition->setArguments([$resourceName]); - $container->setDefinition('lock.'.$resourceName, $lockDefinition); - // provide alias for default resource if ('default' === $resourceName) { - $container->setAlias('lock.store', new Alias('lock.'.$resourceName.'.store', false)); $container->setAlias('lock.factory', new Alias('lock.'.$resourceName.'.factory', false)); - $container->setAlias('lock', new Alias('lock.'.$resourceName, false)); - $container->setAlias(StoreInterface::class, new Alias('lock.store', false)); - $container->setAlias(PersistingStoreInterface::class, new Alias('lock.store', false)); - $container->setAlias(Factory::class, new Alias('lock.factory', false)); $container->setAlias(LockFactory::class, new Alias('lock.factory', false)); - $container->setAlias(LockInterface::class, new Alias('lock', false)); } else { - $container->registerAliasForArgument('lock.'.$resourceName.'.store', StoreInterface::class, $resourceName.'.lock.store'); - $container->registerAliasForArgument('lock.'.$resourceName.'.store', PersistingStoreInterface::class, $resourceName.'.lock.store'); - $container->registerAliasForArgument('lock.'.$resourceName.'.factory', Factory::class, $resourceName.'.lock.factory'); $container->registerAliasForArgument('lock.'.$resourceName.'.factory', LockFactory::class, $resourceName.'.lock.factory'); - $container->registerAliasForArgument('lock.'.$resourceName, LockInterface::class, $resourceName.'.lock'); } } } - private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $validationConfig) + private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $validationConfig) { if (!interface_exists(MessageBusInterface::class)) { throw new LogicException('Messenger support cannot be enabled as the Messenger component is not installed. Try running "composer require symfony/messenger".'); } - $loader->load('messenger.xml'); + $loader->load('messenger.php'); + + if (!interface_exists(DenormalizerInterface::class)) { + $container->removeDefinition('serializer.normalizer.flatten_exception'); + } - if (class_exists(AmqpTransportFactory::class)) { + if (ContainerBuilder::willBeAvailable('symfony/amqp-messenger', AmqpTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { $container->getDefinition('messenger.transport.amqp.factory')->addTag('messenger.transport_factory'); } - if (class_exists(RedisTransportFactory::class)) { + if (ContainerBuilder::willBeAvailable('symfony/redis-messenger', RedisTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { $container->getDefinition('messenger.transport.redis.factory')->addTag('messenger.transport_factory'); } + if (ContainerBuilder::willBeAvailable('symfony/amazon-sqs-messenger', AmazonSqsTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { + $container->getDefinition('messenger.transport.sqs.factory')->addTag('messenger.transport_factory'); + } + + if (ContainerBuilder::willBeAvailable('symfony/beanstalkd-messenger', BeanstalkdTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { + $container->getDefinition('messenger.transport.beanstalkd.factory')->addTag('messenger.transport_factory'); + } + if (null === $config['default_bus'] && 1 === \count($config['buses'])) { $config['default_bus'] = key($config['buses']); } @@ -1792,7 +1935,6 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->register($busId, MessageBus::class)->addArgument([])->addTag('messenger.bus'); if ($busId === $config['default_bus']) { - $container->setAlias('message_bus', $busId)->setPublic(true)->setDeprecated(true, 'The "%alias_id%" service is deprecated, use the "messenger.default_bus" service instead.'); $container->setAlias('messenger.default_bus', $busId)->setPublic(true); $container->setAlias(MessageBusInterface::class, $busId); } else { @@ -1804,6 +1946,9 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->removeDefinition('messenger.transport.symfony_serializer'); $container->removeDefinition('messenger.transport.amqp.factory'); $container->removeDefinition('messenger.transport.redis.factory'); + $container->removeDefinition('messenger.transport.sqs.factory'); + $container->removeDefinition('messenger.transport.beanstalkd.factory'); + $container->removeAlias(SerializerInterface::class); } else { $container->getDefinition('messenger.transport.symfony_serializer') ->replaceArgument(1, $config['serializer']['symfony_serializer']['format']) @@ -1811,15 +1956,38 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->setAlias('messenger.default_serializer', $config['serializer']['default_serializer']); } + $failureTransports = []; + if ($config['failure_transport']) { + if (!isset($config['transports'][$config['failure_transport']])) { + throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport'])); + } + + $container->setAlias('messenger.failure_transports.default', 'messenger.transport.'.$config['failure_transport']); + $failureTransports[] = $config['failure_transport']; + } + + $failureTransportsByName = []; + foreach ($config['transports'] as $name => $transport) { + if ($transport['failure_transport']) { + $failureTransports[] = $transport['failure_transport']; + $failureTransportsByName[$name] = $transport['failure_transport']; + } elseif ($config['failure_transport']) { + $failureTransportsByName[$name] = $config['failure_transport']; + } + } + $senderAliases = []; $transportRetryReferences = []; foreach ($config['transports'] as $name => $transport) { $serializerId = $transport['serializer'] ?? 'messenger.default_serializer'; - $transportDefinition = (new Definition(TransportInterface::class)) ->setFactory([new Reference('messenger.transport_factory'), 'createTransport']) ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)]) - ->addTag('messenger.receiver', ['alias' => $name]) + ->addTag('messenger.receiver', [ + 'alias' => $name, + 'is_failure_transport' => \in_array($name, $failureTransports), + ] + ) ; $container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition); $senderAliases[$name] = $transportId; @@ -1850,6 +2018,18 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $senderReferences[$serviceId] = new Reference($serviceId); } + foreach ($config['transports'] as $name => $transport) { + if ($transport['failure_transport']) { + if (!isset($senderReferences[$transport['failure_transport']])) { + throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $transport['failure_transport'])); + } + } + } + + $failureTransportReferencesByTransportName = array_map(function ($failureTransportName) use ($senderReferences) { + return $senderReferences[$failureTransportName]; + }, $failureTransportsByName); + $messageToSendersMapping = []; foreach ($config['routing'] as $message => $messageConfiguration) { if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { @@ -1880,25 +2060,27 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->getDefinition('messenger.retry_strategy_locator') ->replaceArgument(0, $transportRetryReferences); - if ($config['failure_transport']) { - if (!isset($senderReferences[$config['failure_transport']])) { - throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport'])); - } - - $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener') - ->replaceArgument(0, $senderReferences[$config['failure_transport']]); + if (\count($failureTransports) > 0) { $container->getDefinition('console.command.messenger_failed_messages_retry') ->replaceArgument(0, $config['failure_transport']); $container->getDefinition('console.command.messenger_failed_messages_show') ->replaceArgument(0, $config['failure_transport']); $container->getDefinition('console.command.messenger_failed_messages_remove') ->replaceArgument(0, $config['failure_transport']); + + $failureTransportsByTransportNameServiceLocator = ServiceLocatorTagPass::register($container, $failureTransportReferencesByTransportName); + $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener') + ->replaceArgument(0, $failureTransportsByTransportNameServiceLocator); } else { $container->removeDefinition('messenger.failure.send_failed_message_to_failure_transport_listener'); $container->removeDefinition('console.command.messenger_failed_messages_retry'); $container->removeDefinition('console.command.messenger_failed_messages_show'); $container->removeDefinition('console.command.messenger_failed_messages_remove'); } + + if (!$container->hasDefinition('console.command.messenger_consume_messages')) { + $container->removeDefinition('messenger.listener.reset_services'); + } } private function registerCacheConfiguration(array $config, ContainerBuilder $container) @@ -1919,7 +2101,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con // Inline any env vars referenced in the parameter $container->setParameter('cache.prefix.seed', $container->resolveEnvPlaceholders($container->getParameter('cache.prefix.seed'), true)); } - foreach (['doctrine', 'psr6', 'redis', 'memcached', 'pdo'] as $name) { + foreach (['psr6', 'redis', 'memcached', 'doctrine_dbal', 'pdo'] as $name) { if (isset($config[$name = 'default_'.$name.'_provider'])) { $container->setAlias('cache.'.$name, new Alias(CachePoolPass::getServiceProvider($container, $config[$name]), false)); } @@ -1934,8 +2116,11 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con foreach ($config['pools'] as $name => $pool) { $pool['adapters'] = $pool['adapters'] ?: ['cache.app']; + $isRedisTagAware = ['cache.adapter.redis_tag_aware'] === $pool['adapters']; foreach ($pool['adapters'] as $provider => $adapter) { - if ($config['pools'][$adapter]['tags'] ?? false) { + if (($config['pools'][$adapter]['adapters'] ?? null) === ['cache.adapter.redis_tag_aware']) { + $isRedisTagAware = true; + } elseif ($config['pools'][$adapter]['tags'] ?? false) { $pool['adapters'][$provider] = $adapter = '.'.$adapter.'.inner'; } } @@ -1950,7 +2135,12 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con $pool['reset'] = 'reset'; } - if ($pool['tags']) { + if ($isRedisTagAware && 'cache.app' === $name) { + $container->setAlias('cache.app.taggable', $name); + } elseif ($isRedisTagAware) { + $tagAwareId = $name; + $container->setAlias('.'.$name.'.inner', $name); + } elseif ($pool['tags']) { if (true !== $pool['tags'] && ($config['pools'][$pool['tags']]['tags'] ?? false)) { $pool['tags'] = '.'.$pool['tags'].'.inner'; } @@ -1967,22 +2157,20 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con ->addTag('monolog.logger', ['channel' => 'cache']); } - $pool['name'] = $name; + $pool['name'] = $tagAwareId = $name; $pool['public'] = false; $name = '.'.$name.'.inner'; - - if (!\in_array($pool['name'], ['cache.app', 'cache.system'], true)) { - $container->registerAliasForArgument($pool['name'], TagAwareCacheInterface::class); - $container->registerAliasForArgument($name, CacheInterface::class, $pool['name']); - $container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name']); - } } elseif (!\in_array($name, ['cache.app', 'cache.system'], true)) { - $container->register('.'.$name.'.taggable', TagAwareAdapter::class) + $tagAwareId = '.'.$name.'.taggable'; + $container->register($tagAwareId, TagAwareAdapter::class) ->addArgument(new Reference($name)) ; - $container->registerAliasForArgument('.'.$name.'.taggable', TagAwareCacheInterface::class, $name); - $container->registerAliasForArgument($name, CacheInterface::class); - $container->registerAliasForArgument($name, CacheItemPoolInterface::class); + } + + if (!\in_array($name, ['cache.app', 'cache.system'], true)) { + $container->registerAliasForArgument($tagAwareId, TagAwareCacheInterface::class, $pool['name'] ?? $name); + $container->registerAliasForArgument($name, CacheInterface::class, $pool['name'] ?? $name); + $container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name'] ?? $name); } $definition->setPublic($pool['public']); @@ -2008,23 +2196,29 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con } } - private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $profilerConfig) + private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $profilerConfig) { - $loader->load('http_client.xml'); + $loader->load('http_client.php'); - $container->getDefinition('http_client')->setArguments([$config['default_options'] ?? [], $config['max_host_connections'] ?? 6]); + $options = $config['default_options'] ?? []; + $retryOptions = $options['retry_failed'] ?? ['enabled' => false]; + unset($options['retry_failed']); + $container->getDefinition('http_client')->setArguments([$options, $config['max_host_connections'] ?? 6]); - if (!$hasPsr18 = interface_exists(ClientInterface::class)) { + if (!$hasPsr18 = ContainerBuilder::willBeAvailable('psr/http-client', ClientInterface::class, ['symfony/framework-bundle', 'symfony/http-client'])) { $container->removeDefinition('psr18.http_client'); $container->removeAlias(ClientInterface::class); } - if (!interface_exists(HttpClient::class)) { + if (!ContainerBuilder::willBeAvailable('php-http/httplug', HttpClient::class, ['symfony/framework-bundle', 'symfony/http-client'])) { $container->removeDefinition(HttpClient::class); } - $httpClientId = $this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'; + if ($this->isConfigEnabled($container, $retryOptions)) { + $this->registerRetryableHttpClient($retryOptions, 'http_client', $container); + } + $httpClientId = ($retryOptions['enabled'] ?? false) ? 'http_client.retryable.inner' : ($this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'); foreach ($config['scoped_clients'] as $name => $scopeConfig) { if ('http_client' === $name) { throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name)); @@ -2032,6 +2226,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $scope = $scopeConfig['scope'] ?? null; unset($scopeConfig['scope']); + $retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false]; + unset($scopeConfig['retry_failed']); if (null === $scope) { $baseUri = $scopeConfig['base_uri']; @@ -2049,6 +2245,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder ; } + if ($this->isConfigEnabled($container, $retryOptions)) { + $this->registerRetryableHttpClient($retryOptions, $name, $container); + } + $container->registerAliasForArgument($name, HttpClientInterface::class); if ($hasPsr18) { @@ -2058,16 +2258,59 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->registerAliasForArgument('psr18.'.$name, ClientInterface::class, $name); } } + + if ($responseFactoryId = $config['mock_response_factory'] ?? null) { + $container->register($httpClientId.'.mock_client', MockHttpClient::class) + ->setDecoratedService($httpClientId, null, -10) // lower priority than TraceableHttpClient + ->setArguments([new Reference($responseFactoryId)]); + } + } + + private function registerRetryableHttpClient(array $options, string $name, ContainerBuilder $container) + { + if (!class_exists(RetryableHttpClient::class)) { + throw new LogicException('Support for retrying failed requests requires symfony/http-client 5.2 or higher, try upgrading.'); + } + + if (null !== $options['retry_strategy']) { + $retryStrategy = new Reference($options['retry_strategy']); + } else { + $retryStrategy = new ChildDefinition('http_client.abstract_retry_strategy'); + $codes = []; + foreach ($options['http_codes'] as $code => $codeOptions) { + if ($codeOptions['methods']) { + $codes[$code] = $codeOptions['methods']; + } else { + $codes[] = $code; + } + } + + $retryStrategy + ->replaceArgument(0, $codes ?: GenericRetryStrategy::DEFAULT_RETRY_STATUS_CODES) + ->replaceArgument(1, $options['delay']) + ->replaceArgument(2, $options['multiplier']) + ->replaceArgument(3, $options['max_delay']) + ->replaceArgument(4, $options['jitter']); + $container->setDefinition($name.'.retry_strategy', $retryStrategy); + + $retryStrategy = new Reference($name.'.retry_strategy'); + } + + $container + ->register($name.'.retryable', RetryableHttpClient::class) + ->setDecoratedService($name, null, 10) // higher priority than TraceableHttpClient + ->setArguments([new Reference($name.'.retryable.inner'), $retryStrategy, $options['max_retries'], new Reference('logger')]) + ->addTag('monolog.logger', ['channel' => 'http_client']); } - private function registerMailerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerMailerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) { if (!class_exists(Mailer::class)) { throw new LogicException('Mailer support cannot be enabled as the component is not installed. Try running "composer require symfony/mailer".'); } - $loader->load('mailer.xml'); - $loader->load('mailer_transports.xml'); + $loader->load('mailer.php'); + $loader->load('mailer_transports.php'); if (!\count($config['transports']) && null === $config['dsn']) { $config['dsn'] = 'smtp://null'; } @@ -2075,38 +2318,275 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $container->getDefinition('mailer.transports')->setArgument(0, $transports); $container->getDefinition('mailer.default_transport')->setArgument(0, current($transports)); + $mailer = $container->getDefinition('mailer.mailer'); + if (false === $messageBus = $config['message_bus']) { + $mailer->replaceArgument(1, null); + } else { + $mailer->replaceArgument(1, $messageBus ? new Reference($messageBus) : new Reference('messenger.default_bus', ContainerInterface::NULL_ON_INVALID_REFERENCE)); + } + $classToServices = [ - SesTransportFactory::class => 'mailer.transport_factory.amazon', GmailTransportFactory::class => 'mailer.transport_factory.gmail', - MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', MailgunTransportFactory::class => 'mailer.transport_factory.mailgun', + MailjetTransportFactory::class => 'mailer.transport_factory.mailjet', + MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', + SendinblueTransportFactory::class => 'mailer.transport_factory.sendinblue', + SesTransportFactory::class => 'mailer.transport_factory.amazon', + OhMySmtpTransportFactory::class => 'mailer.transport_factory.ohmysmtp', ]; foreach ($classToServices as $class => $service) { - if (!class_exists($class)) { + $package = substr($service, \strlen('mailer.transport_factory.')); + + if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-mailer', 'gmail' === $package ? 'google' : $package), $class, ['symfony/framework-bundle', 'symfony/mailer'])) { $container->removeDefinition($service); } } - $recipients = $config['envelope']['recipients'] ?? null; - $sender = $config['envelope']['sender'] ?? null; - $envelopeListener = $container->getDefinition('mailer.envelope_listener'); - $envelopeListener->setArgument(0, $sender); - $envelopeListener->setArgument(1, $recipients); + $envelopeListener->setArgument(0, $config['envelope']['sender'] ?? null); + $envelopeListener->setArgument(1, $config['envelope']['recipients'] ?? null); + + if ($config['headers']) { + $headers = new Definition(Headers::class); + foreach ($config['headers'] as $name => $data) { + $value = $data['value']; + if (\in_array(strtolower($name), ['from', 'to', 'cc', 'bcc', 'reply-to'])) { + $value = (array) $value; + } + $headers->addMethodCall('addHeader', [$name, $value]); + } + $messageListener = $container->getDefinition('mailer.message_listener'); + $messageListener->setArgument(0, $headers); + } else { + $container->removeDefinition('mailer.message_listener'); + } + } + + private function registerNotifierConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) + { + if (!class_exists(Notifier::class)) { + throw new LogicException('Notifier support cannot be enabled as the component is not installed. Try running "composer require symfony/notifier".'); + } + + $loader->load('notifier.php'); + $loader->load('notifier_transports.php'); + + if ($config['chatter_transports']) { + $container->getDefinition('chatter.transports')->setArgument(0, $config['chatter_transports']); + } else { + $container->removeDefinition('chatter'); + } + if ($config['texter_transports']) { + $container->getDefinition('texter.transports')->setArgument(0, $config['texter_transports']); + } else { + $container->removeDefinition('texter'); + } + + if ($this->mailerConfigEnabled) { + $sender = $container->getDefinition('mailer.envelope_listener')->getArgument(0); + $container->getDefinition('notifier.channel.email')->setArgument(2, $sender); + } else { + $container->removeDefinition('notifier.channel.email'); + } + + if ($this->messengerConfigEnabled) { + if ($config['notification_on_failed_messages']) { + $container->getDefinition('notifier.failed_message_listener')->addTag('kernel.event_subscriber'); + } + + // as we have a bus, the channels don't need the transports + $container->getDefinition('notifier.channel.chat')->setArgument(0, null); + if ($container->hasDefinition('notifier.channel.email')) { + $container->getDefinition('notifier.channel.email')->setArgument(0, null); + } + $container->getDefinition('notifier.channel.sms')->setArgument(0, null); + $container->getDefinition('notifier.channel.push')->setArgument(0, null); + } + + $container->getDefinition('notifier.channel_policy')->setArgument(0, $config['channel_policy']); + + $container->registerForAutoconfiguration(NotifierTransportFactoryInterface::class) + ->addTag('chatter.transport_factory'); + + $container->registerForAutoconfiguration(NotifierTransportFactoryInterface::class) + ->addTag('texter.transport_factory'); + + $classToServices = [ + AllMySmsTransportFactory::class => 'notifier.transport_factory.all-my-sms', + AmazonSnsTransportFactory::class => 'notifier.transport_factory.amazon-sns', + ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell', + DiscordTransportFactory::class => 'notifier.transport_factory.discord', + EsendexTransportFactory::class => 'notifier.transport_factory.esendex', + ExpoTransportFactory::class => 'notifier.transport_factory.expo', + FakeChatTransportFactory::class => 'notifier.transport_factory.fake-chat', + FakeSmsTransportFactory::class => 'notifier.transport_factory.fake-sms', + FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', + FreeMobileTransportFactory::class => 'notifier.transport_factory.free-mobile', + GatewayApiTransportFactory::class => 'notifier.transport_factory.gateway-api', + GitterTransportFactory::class => 'notifier.transport_factory.gitter', + GoogleChatTransportFactory::class => 'notifier.transport_factory.google-chat', + InfobipTransportFactory::class => 'notifier.transport_factory.infobip', + IqsmsTransportFactory::class => 'notifier.transport_factory.iqsms', + LightSmsTransportFactory::class => 'notifier.transport_factory.light-sms', + LinkedInTransportFactory::class => 'notifier.transport_factory.linked-in', + MailjetNotifierTransportFactory::class => 'notifier.transport_factory.mailjet', + MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', + MercureTransportFactory::class => 'notifier.transport_factory.mercure', + MessageBirdTransport::class => 'notifier.transport_factory.message-bird', + MessageMediaTransportFactory::class => 'notifier.transport_factory.message-media', + MicrosoftTeamsTransportFactory::class => 'notifier.transport_factory.microsoft-teams', + MobytTransportFactory::class => 'notifier.transport_factory.mobyt', + OctopushTransportFactory::class => 'notifier.transport_factory.octopush', + OneSignalTransportFactory::class => 'notifier.transport_factory.one-signal', + OvhCloudTransportFactory::class => 'notifier.transport_factory.ovh-cloud', + RocketChatTransportFactory::class => 'notifier.transport_factory.rocket-chat', + SendinblueNotifierTransportFactory::class => 'notifier.transport_factory.sendinblue', + SinchTransportFactory::class => 'notifier.transport_factory.sinch', + SlackTransportFactory::class => 'notifier.transport_factory.slack', + Sms77TransportFactory::class => 'notifier.transport_factory.sms77', + SmsapiTransportFactory::class => 'notifier.transport_factory.smsapi', + SmsBiurasTransportFactory::class => 'notifier.transport_factory.sms-biuras', + SmscTransportFactory::class => 'notifier.transport_factory.smsc', + SpotHitTransportFactory::class => 'notifier.transport_factory.spot-hit', + TelegramTransportFactory::class => 'notifier.transport_factory.telegram', + TelnyxTransportFactory::class => 'notifier.transport_factory.telnyx', + TurboSmsTransport::class => 'notifier.transport_factory.turbo-sms', + TwilioTransportFactory::class => 'notifier.transport_factory.twilio', + VonageTransportFactory::class => 'notifier.transport_factory.vonage', + YunpianTransportFactory::class => 'notifier.transport_factory.yunpian', + ZulipTransportFactory::class => 'notifier.transport_factory.zulip', + ]; + + $parentPackages = ['symfony/framework-bundle', 'symfony/notifier']; + + foreach ($classToServices as $class => $service) { + $package = substr($service, \strlen('notifier.transport_factory.')); + + if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-notifier', $package), $class, $parentPackages)) { + $container->removeDefinition($service); + } + } + + if (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages) && ContainerBuilder::willBeAvailable('symfony/mercure-bundle', MercureBundle::class, $parentPackages)) { + $container->getDefinition($classToServices[MercureTransportFactory::class]) + ->replaceArgument('$registry', new Reference(HubRegistry::class)); + } elseif (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages)) { + $container->removeDefinition($classToServices[MercureTransportFactory::class]); + } + + if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { + $container->getDefinition($classToServices[FakeChatTransportFactory::class]) + ->replaceArgument('$mailer', new Reference('mailer')) + ->replaceArgument('$logger', new Reference('logger')); + } + + if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { + $container->getDefinition($classToServices[FakeSmsTransportFactory::class]) + ->replaceArgument('$mailer', new Reference('mailer')) + ->replaceArgument('$logger', new Reference('logger')); + } + + if (isset($config['admin_recipients'])) { + $notifier = $container->getDefinition('notifier'); + foreach ($config['admin_recipients'] as $i => $recipient) { + $id = 'notifier.admin_recipient.'.$i; + $container->setDefinition($id, new Definition(Recipient::class, [$recipient['email'], $recipient['phone']])); + $notifier->addMethodCall('addAdminRecipient', [new Reference($id)]); + } + } + } + + private function registerRateLimiterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) + { + $loader->load('rate_limiter.php'); + + foreach ($config['limiters'] as $name => $limiterConfig) { + self::registerRateLimiter($container, $name, $limiterConfig); + } + } + + public static function registerRateLimiter(ContainerBuilder $container, string $name, array $limiterConfig) + { + // default configuration (when used by other DI extensions) + $limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter']; + + $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')); + + if (null !== $limiterConfig['lock_factory']) { + if (!self::$lockConfigEnabled) { + throw new LogicException(sprintf('Rate limiter "%s" requires the Lock component to be installed and configured.', $name)); + } + + $limiter->replaceArgument(2, new Reference($limiterConfig['lock_factory'])); + } + unset($limiterConfig['lock_factory']); + + $storageId = $limiterConfig['storage_service'] ?? null; + if (null === $storageId) { + $container->register($storageId = 'limiter.storage.'.$name, CacheStorage::class)->addArgument(new Reference($limiterConfig['cache_pool'])); + } + + $limiter->replaceArgument(1, new Reference($storageId)); + unset($limiterConfig['storage_service']); + unset($limiterConfig['cache_pool']); + + $limiterConfig['id'] = $name; + $limiter->replaceArgument(0, $limiterConfig); + + $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); + } + + private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader) + { + $loader->load('uid.php'); + + $container->getDefinition('uuid.factory') + ->setArguments([ + $config['default_uuid_version'], + $config['time_based_uuid_version'], + $config['name_based_uuid_version'], + UuidV4::class, + $config['time_based_uuid_node'] ?? null, + $config['name_based_uuid_namespace'] ?? null, + ]) + ; + + if (isset($config['name_based_uuid_namespace'])) { + $container->getDefinition('name_based_uuid.factory') + ->setArguments([$config['name_based_uuid_namespace']]); + } + } + + private function resolveTrustedHeaders(array $headers): int + { + $trustedHeaders = 0; + + foreach ($headers as $h) { + switch ($h) { + case 'forwarded': $trustedHeaders |= Request::HEADER_FORWARDED; break; + case 'x-forwarded-for': $trustedHeaders |= Request::HEADER_X_FORWARDED_FOR; break; + case 'x-forwarded-host': $trustedHeaders |= Request::HEADER_X_FORWARDED_HOST; break; + case 'x-forwarded-proto': $trustedHeaders |= Request::HEADER_X_FORWARDED_PROTO; break; + case 'x-forwarded-port': $trustedHeaders |= Request::HEADER_X_FORWARDED_PORT; break; + case 'x-forwarded-prefix': $trustedHeaders |= Request::HEADER_X_FORWARDED_PREFIX; break; + } + } + + return $trustedHeaders; } /** * {@inheritdoc} */ - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string|false { return \dirname(__DIR__).'/Resources/config/schema'; } - public function getNamespace() + public function getNamespace(): string { return 'http://symfony.com/schema/dic/symfony'; } diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php deleted file mode 100644 index ef146419013bc..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\EventListener; - -use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\KernelEvents; - -/** - * Guarantees that the _controller key is parsed into its final format. - * - * @author Ryan Weaver - * - * @method onKernelRequest(RequestEvent $event) - * - * @deprecated since Symfony 4.1 - */ -class ResolveControllerNameSubscriber implements EventSubscriberInterface -{ - private $parser; - - public function __construct(ControllerNameParser $parser, bool $triggerDeprecation = true) - { - if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); - } - - $this->parser = $parser; - } - - /** - * @internal - */ - public function resolveControllerName(...$args) - { - $this->onKernelRequest(...$args); - } - - public function __call(string $method, array $args) - { - if ('onKernelRequest' !== $method && 'onkernelrequest' !== strtolower($method)) { - throw new \Error(sprintf('Error: Call to undefined method "%s::%s()".', static::class, $method)); - } - - $event = $args[0]; - - $controller = $event->getRequest()->attributes->get('_controller'); - if (\is_string($controller) && !str_contains($controller, '::') && 2 === substr_count($controller, ':')) { - // controller in the a:b:c notation then - $event->getRequest()->attributes->set('_controller', $parsedNotation = $this->parser->parse($controller, false)); - - @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $controller, $parsedNotation), \E_USER_DEPRECATED); - } - } - - public static function getSubscribedEvents() - { - return [ - KernelEvents::REQUEST => ['resolveControllerName', 24], - ]; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php index 231329c0bf07c..53cae12ebbcff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php @@ -39,8 +39,7 @@ final class SuggestMissingPackageSubscriber implements EventSubscriberInterface '_default' => ['MakerBundle', 'symfony/maker-bundle --dev'], ], 'server' => [ - 'dump' => ['Debug Bundle', 'symfony/debug-bundle --dev'], - '_default' => ['WebServerBundle', 'symfony/web-server-bundle --dev'], + '_default' => ['Debug Bundle', 'symfony/debug-bundle --dev'], ], ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index f5c52cf19749d..794686e3cdf5f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -14,12 +14,12 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SessionPass; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; @@ -34,8 +34,8 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass; use Symfony\Component\Config\Resource\ClassExistenceResource; +use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; -use Symfony\Component\Debug\ErrorHandler as LegacyErrorHandler; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -90,31 +90,33 @@ class FrameworkBundle extends Bundle public function boot() { ErrorHandler::register(null, false)->throwAt($this->container->getParameter('debug.error_handler.throw_at'), true); - if (class_exists(LegacyErrorHandler::class, false)) { - LegacyErrorHandler::register(null, false)->throwAt($this->container->getParameter('debug.error_handler.throw_at'), true); - } if ($this->container->getParameter('kernel.http_method_override')) { Request::enableHttpMethodParameterOverride(); } - - if ($trustedHosts = $this->container->getParameter('kernel.trusted_hosts')) { - Request::setTrustedHosts($trustedHosts); - } } public function build(ContainerBuilder $container) { parent::build($container); - $hotPathEvents = [ + $registerListenersPass = new RegisterListenersPass(); + $registerListenersPass->setHotPathEvents([ KernelEvents::REQUEST, KernelEvents::CONTROLLER, KernelEvents::CONTROLLER_ARGUMENTS, KernelEvents::RESPONSE, KernelEvents::FINISH_REQUEST, - ]; + ]); + if (class_exists(ConsoleEvents::class)) { + $registerListenersPass->setNoPreloadEvents([ + ConsoleEvents::COMMAND, + ConsoleEvents::TERMINATE, + ConsoleEvents::ERROR, + ]); + } + $container->addCompilerPass(new AssetsContextPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); @@ -123,8 +125,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new ProfilerPass()); // must be registered before removing private services as some might be listeners/subscribers // but as late as possible to get resolved parameters - $container->addCompilerPass((new RegisterListenersPass())->setHotPathEvents($hotPathEvents), PassConfig::TYPE_BEFORE_REMOVING); - $container->addCompilerPass(new TemplatingPass()); + $container->addCompilerPass($registerListenersPass, PassConfig::TYPE_BEFORE_REMOVING); $this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class); $container->addCompilerPass(new AddAnnotationsCachedReaderPass(), PassConfig::TYPE_AFTER_REMOVING, -255); $this->addCompilerPassIfExists($container, AddValidatorInitializersPass::class); @@ -156,7 +157,7 @@ public function build(ContainerBuilder $container) $this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class); $container->addCompilerPass(new RegisterReverseContainerPass(true)); $container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING); - $container->addCompilerPass(new SessionPass()); + $container->addCompilerPass(new RemoveUnusedSessionMarshallingHandlerPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 03cd73b4f8994..1e50bd56b6337 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -16,6 +16,8 @@ use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache as BaseHttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\HttpCache\StoreInterface; +use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface; use Symfony\Component\HttpKernel\KernelInterface; /** @@ -28,57 +30,62 @@ class HttpCache extends BaseHttpCache protected $cacheDir; protected $kernel; + private $store = null; + private $surrogate; + private array $options; + /** - * @param string $cacheDir The cache directory (default used if null) + * @param $cache The cache directory (default used if null) or the storage instance */ - public function __construct(KernelInterface $kernel, string $cacheDir = null) + public function __construct(KernelInterface $kernel, string|StoreInterface $cache = null, SurrogateInterface $surrogate = null, array $options = null) { $this->kernel = $kernel; - $this->cacheDir = $cacheDir; + $this->surrogate = $surrogate; + $this->options = $options ?? []; + + if ($cache instanceof StoreInterface) { + $this->store = $cache; + } else { + $this->cacheDir = $cache; + } - $isDebug = $kernel->isDebug(); - $options = ['debug' => $isDebug]; + if (null === $options && $kernel->isDebug()) { + $this->options = ['debug' => true]; + } - if ($isDebug) { - $options['stale_if_error'] = 0; + if ($this->options['debug'] ?? false) { + $this->options += ['stale_if_error' => 0]; } - parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($options, $this->getOptions())); + parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($this->options, $this->getOptions())); } /** - * Forwards the Request to the backend and returns the Response. - * - * @param bool $raw Whether to catch exceptions or not - * @param Response $entry A Response instance (the stale entry if present, null otherwise) - * - * @return Response A Response instance + * {@inheritdoc} */ - protected function forward(Request $request, $raw = false, Response $entry = null) + protected function forward(Request $request, bool $catch = false, Response $entry = null): Response { $this->getKernel()->boot(); $this->getKernel()->getContainer()->set('cache', $this); - return parent::forward($request, $raw, $entry); + return parent::forward($request, $catch, $entry); } /** * Returns an array of options to customize the Cache configuration. - * - * @return array An array of options */ - protected function getOptions() + protected function getOptions(): array { return []; } - protected function createSurrogate() + protected function createSurrogate(): SurrogateInterface { - return new Esi(); + return $this->surrogate ?? new Esi(); } - protected function createStore() + protected function createStore(): StoreInterface { - return new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache'); + return $this->store ?? new Store($this->cacheDir ?: $this->kernel->getCacheDir().'/http_cache'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php index 6f4f3a7dec17b..59e5d2efb6aec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -13,8 +13,13 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Routing\RouteCollectionBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader as ContainerPhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Routing\Loader\PhpFileLoader as RoutingPhpFileLoader; +use Symfony\Component\Routing\RouteCollection; /** * A Kernel that provides configuration hooks. @@ -24,32 +29,109 @@ */ trait MicroKernelTrait { - /** - * Add or import routes into your application. - * - * $routes->import('config/routing.yml'); - * $routes->add('/admin', 'App\Controller\AdminController::dashboard', 'admin_dashboard'); - */ - abstract protected function configureRoutes(RouteCollectionBuilder $routes); - /** * Configures the container. * * You can register extensions: * - * $container->loadFromExtension('framework', [ + * $container->extension('framework', [ * 'secret' => '%secret%' * ]); * * Or services: * - * $container->register('halloween', 'FooBundle\HalloweenProvider'); + * $container->services()->set('halloween', 'FooBundle\HalloweenProvider'); * * Or parameters: * - * $container->setParameter('halloween', 'lot of fun'); + * $container->parameters()->set('halloween', 'lot of fun'); + */ + private function configureContainer(ContainerConfigurator $container, LoaderInterface $loader, ContainerBuilder $builder): void + { + $configDir = $this->getConfigDir(); + + $container->import($configDir.'/{packages}/*.yaml'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.yaml'); + + if (is_file($configDir.'/services.yaml')) { + $container->import($configDir.'/services.yaml'); + $container->import($configDir.'/{services}_'.$this->environment.'.yaml'); + } else { + $container->import($configDir.'/{services}.php'); + } + } + + /** + * Adds or imports routes into your application. + * + * $routes->import($this->getConfigDir().'/*.{yaml,php}'); + * $routes + * ->add('admin_dashboard', '/admin') + * ->controller('App\Controller\AdminController::dashboard') + * ; + */ + private function configureRoutes(RoutingConfigurator $routes): void + { + $configDir = $this->getConfigDir(); + + $routes->import($configDir.'/{routes}/'.$this->environment.'/*.yaml'); + $routes->import($configDir.'/{routes}/*.yaml'); + + if (is_file($configDir.'/routes.yaml')) { + $routes->import($configDir.'/routes.yaml'); + } else { + $routes->import($configDir.'/{routes}.php'); + } + } + + /** + * Gets the path to the configuration directory. */ - abstract protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader); + private function getConfigDir(): string + { + return $this->getProjectDir().'/config'; + } + + /** + * Gets the path to the bundles configuration file. + */ + private function getBundlesPath(): string + { + return $this->getConfigDir().'/bundles.php'; + } + + /** + * {@inheritdoc} + */ + public function getCacheDir(): string + { + if (isset($_SERVER['APP_CACHE_DIR'])) { + return $_SERVER['APP_CACHE_DIR'].'/'.$this->environment; + } + + return parent::getCacheDir(); + } + + /** + * {@inheritdoc} + */ + public function getLogDir(): string + { + return $_SERVER['APP_LOG_DIR'] ?? parent::getLogDir(); + } + + /** + * {@inheritdoc} + */ + public function registerBundles(): iterable + { + $contents = require $this->getBundlesPath(); + foreach ($contents as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } /** * {@inheritdoc} @@ -64,8 +146,12 @@ public function registerContainerConfiguration(LoaderInterface $loader) ], ]); + $kernelClass = false !== strpos(static::class, "@anonymous\0") ? parent::class : static::class; + if (!$container->hasDefinition('kernel')) { - $container->register('kernel', static::class) + $container->register('kernel', $kernelClass) + ->addTag('controller.service_arguments') + ->setAutoconfigured(true) ->setSynthetic(true) ->setPublic(true) ; @@ -74,24 +160,63 @@ public function registerContainerConfiguration(LoaderInterface $loader) $kernelDefinition = $container->getDefinition('kernel'); $kernelDefinition->addTag('routing.route_loader'); - if ($this instanceof EventSubscriberInterface) { - $kernelDefinition->addTag('kernel.event_subscriber'); + $container->addObjectResource($this); + $container->fileExists($this->getBundlesPath()); + + $configureContainer = new \ReflectionMethod($this, 'configureContainer'); + $configuratorClass = $configureContainer->getNumberOfParameters() > 0 && ($type = $configureContainer->getParameters()[0]->getType()) instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null; + + if ($configuratorClass && !is_a(ContainerConfigurator::class, $configuratorClass, true)) { + $configureContainer->getClosure($this)($container, $loader); + + return; } - $this->configureContainer($container, $loader); + $file = (new \ReflectionObject($this))->getFileName(); + /* @var ContainerPhpFileLoader $kernelLoader */ + $kernelLoader = $loader->getResolver()->resolve($file); + $kernelLoader->setCurrentDir(\dirname($file)); + $instanceof = &\Closure::bind(function &() { return $this->instanceof; }, $kernelLoader, $kernelLoader)(); - $container->addObjectResource($this); + $valuePreProcessor = AbstractConfigurator::$valuePreProcessor; + AbstractConfigurator::$valuePreProcessor = function ($value) { + return $this === $value ? new Reference('kernel') : $value; + }; + + try { + $configureContainer->getClosure($this)(new ContainerConfigurator($container, $kernelLoader, $instanceof, $file, $file, $this->getEnvironment()), $loader, $container); + } finally { + $instanceof = []; + $kernelLoader->registerAliasesForSinglyImplementedInterfaces(); + AbstractConfigurator::$valuePreProcessor = $valuePreProcessor; + } + + $container->setAlias($kernelClass, 'kernel')->setPublic(true); }); } /** * @internal */ - public function loadRoutes(LoaderInterface $loader) + public function loadRoutes(LoaderInterface $loader): RouteCollection { - $routes = new RouteCollectionBuilder($loader); - $this->configureRoutes($routes); + $file = (new \ReflectionObject($this))->getFileName(); + /* @var RoutingPhpFileLoader $kernelLoader */ + $kernelLoader = $loader->getResolver()->resolve($file, 'php'); + $kernelLoader->setCurrentDir(\dirname($file)); + $collection = new RouteCollection(); + + $configureRoutes = new \ReflectionMethod($this, 'configureRoutes'); + $configureRoutes->getClosure($this)(new RoutingConfigurator($collection, $kernelLoader, $file, $file, $this->getEnvironment())); + + foreach ($collection as $route) { + $controller = $route->getDefault('_controller'); + + if (\is_array($controller) && [0, 1] === array_keys($controller) && $this === $controller[0]) { + $route->setDefault('_controller', ['kernel', $controller[1]]); + } + } - return $routes->build(); + return $collection; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php index b05b60def12d1..c80177c5d39c0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php +++ b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php @@ -11,11 +11,237 @@ namespace Symfony\Bundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Test\TestBrowserToken; +use Symfony\Component\BrowserKit\Cookie; +use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\BrowserKit\History; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelBrowser; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile; +use Symfony\Component\Security\Core\User\UserInterface; + /** - * Client simulates a browser and makes requests to a Kernel object. + * Simulates a browser and makes requests to a Kernel object. * * @author Fabien Potencier */ -class KernelBrowser extends Client +class KernelBrowser extends HttpKernelBrowser { + private bool $hasPerformedRequest = false; + private bool $profiler = false; + private bool $reboot = true; + + /** + * {@inheritdoc} + */ + public function __construct(KernelInterface $kernel, array $server = [], History $history = null, CookieJar $cookieJar = null) + { + parent::__construct($kernel, $server, $history, $cookieJar); + } + + /** + * Returns the container. + */ + public function getContainer(): ContainerInterface + { + $container = $this->kernel->getContainer(); + + return $container->has('test.service_container') ? $container->get('test.service_container') : $container; + } + + /** + * Returns the kernel. + */ + public function getKernel(): KernelInterface + { + return $this->kernel; + } + + /** + * Gets the profile associated with the current Response. + */ + public function getProfile(): HttpProfile|false|null + { + if (null === $this->response || !$this->getContainer()->has('profiler')) { + return false; + } + + return $this->getContainer()->get('profiler')->loadProfileFromResponse($this->response); + } + + /** + * Enables the profiler for the very next request. + * + * If the profiler is not enabled, the call to this method does nothing. + */ + public function enableProfiler() + { + if ($this->getContainer()->has('profiler')) { + $this->profiler = true; + } + } + + /** + * Disables kernel reboot between requests. + * + * By default, the Client reboots the Kernel for each request. This method + * allows to keep the same kernel across requests. + */ + public function disableReboot() + { + $this->reboot = false; + } + + /** + * Enables kernel reboot between requests. + */ + public function enableReboot() + { + $this->reboot = true; + } + + /** + * @param UserInterface $user + * + * @return $this + */ + public function loginUser(object $user, string $firewallContext = 'main'): static + { + if (!interface_exists(UserInterface::class)) { + throw new \LogicException(sprintf('"%s" requires symfony/security-core to be installed.', __METHOD__)); + } + + if (!$user instanceof UserInterface) { + throw new \LogicException(sprintf('The first argument of "%s" must be instance of "%s", "%s" provided.', __METHOD__, UserInterface::class, \is_object($user) ? \get_class($user) : \gettype($user))); + } + + $token = new TestBrowserToken($user->getRoles(), $user, $firewallContext); + // required for compatibilty with Symfony 5.4 + if (method_exists($token, 'isAuthenticated')) { + $token->setAuthenticated(true, false); + } + + $container = $this->getContainer(); + $container->get('security.untracked_token_storage')->setToken($token); + + if (!$container->has('session.factory')) { + return $this; + } + + $session = $container->get('session.factory')->createSession(); + $session->set('_security_'.$firewallContext, serialize($token)); + $session->save(); + + $domains = array_unique(array_map(function (Cookie $cookie) use ($session) { + return $cookie->getName() === $session->getName() ? $cookie->getDomain() : ''; + }, $this->getCookieJar()->all())) ?: ['']; + foreach ($domains as $domain) { + $cookie = new Cookie($session->getName(), $session->getId(), null, null, $domain); + $this->getCookieJar()->set($cookie); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @param Request $request + */ + protected function doRequest(object $request): Response + { + // avoid shutting down the Kernel if no request has been performed yet + // WebTestCase::createClient() boots the Kernel but do not handle a request + if ($this->hasPerformedRequest && $this->reboot) { + $this->kernel->boot(); + $this->kernel->shutdown(); + } else { + $this->hasPerformedRequest = true; + } + + if ($this->profiler) { + $this->profiler = false; + + $this->kernel->boot(); + $this->getContainer()->get('profiler')->enable(); + } + + return parent::doRequest($request); + } + + /** + * {@inheritdoc} + * + * @param Request $request + */ + protected function doRequestInProcess(object $request): Response + { + $response = parent::doRequestInProcess($request); + + $this->profiler = false; + + return $response; + } + + /** + * Returns the script to execute when the request must be insulated. + * + * It assumes that the autoloader is named 'autoload.php' and that it is + * stored in the same directory as the kernel (this is the case for the + * Symfony Standard Edition). If this is not your case, create your own + * client and override this method. + * + * @param Request $request + */ + protected function getScript(object $request): string + { + $kernel = var_export(serialize($this->kernel), true); + $request = var_export(serialize($request), true); + $errorReporting = error_reporting(); + + $requires = ''; + foreach (get_declared_classes() as $class) { + if (str_starts_with($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $file = \dirname($r->getFileName(), 2).'/autoload.php'; + if (is_file($file)) { + $requires .= 'require_once '.var_export($file, true).";\n"; + } + } + } + + if (!$requires) { + throw new \RuntimeException('Composer autoloader not found.'); + } + + $requires .= 'require_once '.var_export((new \ReflectionObject($this->kernel))->getFileName(), true).";\n"; + + $profilerCode = ''; + if ($this->profiler) { + $profilerCode = <<<'EOF' +$container = $kernel->getContainer(); +$container = $container->has('test.service_container') ? $container->get('test.service_container') : $container; +$container->get('profiler')->enable(); +EOF; + } + + $code = <<boot(); +$profilerCode + +\$request = unserialize($request); +EOF; + + return $code.$this->getHandleScript(); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php b/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php index ec9ae1f97c0ff..4920c43ebe182 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php @@ -15,5 +15,5 @@ $target = dirname(__DIR__, 2).'/DependencyInjection/Compiler/UnusedTagsPass.php'; $contents = file_get_contents($target); -$contents = preg_replace('{private \$knownTags = \[(.+?)\];}sm', "private \$knownTags = [\n '".implode("',\n '", UnusedTagsPassUtils::getDefinedTags())."',\n ];", $contents); +$contents = preg_replace('{private const KNOWN_TAGS = \[(.+?)\];}sm', "private const KNOWN_TAGS = [\n '".implode("',\n '", UnusedTagsPassUtils::getDefinedTags())."',\n ];", $contents); file_put_contents($target, $contents); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php new file mode 100644 index 0000000000000..fd25a3ab2fb2e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; +use Doctrine\Common\Annotations\PsrCachedReader; +use Doctrine\Common\Annotations\Reader; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\AnnotationsCacheWarmer; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('annotations.reader', AnnotationReader::class) + ->call('addGlobalIgnoredName', [ + 'required', + service('annotations.dummy_registry'), // dummy arg to register class_exists as annotation loader only when required + ]) + + ->set('annotations.dummy_registry', AnnotationRegistry::class) + ->call('registerUniqueLoader', ['class_exists']) + + ->set('annotations.cached_reader', PsrCachedReader::class) + ->args([ + service('annotations.reader'), + inline_service(ArrayAdapter::class), + abstract_arg('Debug-Flag'), + ]) + + ->set('annotations.filesystem_cache_adapter', FilesystemAdapter::class) + ->args([ + '', + 0, + abstract_arg('Cache-Directory'), + ]) + + ->set('annotations.cache_warmer', AnnotationsCacheWarmer::class) + ->args([ + service('annotations.reader'), + param('kernel.cache_dir').'/annotations.php', + '#^Symfony\\\\(?:Component\\\\HttpKernel\\\\|Bundle\\\\FrameworkBundle\\\\Controller\\\\(?!.*Controller$))#', + param('kernel.debug'), + ]) + + ->set('annotations.cache_adapter', PhpArrayAdapter::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([ + param('kernel.cache_dir').'/annotations.php', + service('cache.annotations'), + ]) + ->tag('container.hot_path') + + ->alias('annotation_reader', 'annotations.reader') + ->alias(Reader::class, 'annotation_reader'); +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml deleted file mode 100644 index 4420dfbf00db1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - required - - - - - - - - class_exists - - - - - - - - - - - - - - - - - - 0 - - - - - - - - - - %kernel.cache_dir%/annotations.php - #^Symfony\\(?:Component\\HttpKernel\\|Bundle\\FrameworkBundle\\Controller\\(?!.*Controller$))# - %kernel.debug% - - - - - %kernel.cache_dir%/annotations.php - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php new file mode 100644 index 0000000000000..2457121864afc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Asset\Context\RequestStackContext; +use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\Packages; +use Symfony\Component\Asset\PathPackage; +use Symfony\Component\Asset\UrlPackage; +use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('asset.request_context.base_path', null) + ->set('asset.request_context.secure', null) + ; + + $container->services() + ->set('assets.packages', Packages::class) + ->args([ + service('assets._default_package'), + tagged_iterator('assets.package', 'package'), + ]) + + ->alias(Packages::class, 'assets.packages') + + ->set('assets.empty_package', Package::class) + ->args([ + service('assets.empty_version_strategy'), + ]) + + ->alias('assets._default_package', 'assets.empty_package') + + ->set('assets.context', RequestStackContext::class) + ->args([ + service('request_stack'), + param('asset.request_context.base_path'), + param('asset.request_context.secure'), + ]) + + ->set('assets.path_package', PathPackage::class) + ->abstract() + ->args([ + abstract_arg('base path'), + abstract_arg('version strategy'), + service('assets.context'), + ]) + + ->set('assets.url_package', UrlPackage::class) + ->abstract() + ->args([ + abstract_arg('base URLs'), + abstract_arg('version strategy'), + service('assets.context'), + ]) + + ->set('assets.static_version_strategy', StaticVersionStrategy::class) + ->abstract() + ->args([ + abstract_arg('version'), + abstract_arg('format'), + ]) + + ->set('assets.empty_version_strategy', EmptyVersionStrategy::class) + + ->set('assets.json_manifest_version_strategy', JsonManifestVersionStrategy::class) + ->abstract() + ->args([ + abstract_arg('manifest path'), + service('http_client')->nullOnInvalid(), + false, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml deleted file mode 100644 index 4aaa702df5dc9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/assets.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - false - - - - - - - - - - - - - - - - - - %asset.request_context.base_path% - %asset.request_context.secure% - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php new file mode 100644 index 0000000000000..d1a10e2b36c4b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\MemcachedAdapter; +use Symfony\Component\Cache\Adapter\PdoAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Messenger\EarlyExpirationHandler; +use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('cache.app') + ->parent('cache.adapter.filesystem') + ->public() + ->tag('cache.pool', ['clearer' => 'cache.app_clearer']) + + ->set('cache.app.taggable', TagAwareAdapter::class) + ->args([service('cache.app')]) + + ->set('cache.system') + ->parent('cache.adapter.system') + ->public() + ->tag('cache.pool') + + ->set('cache.validator') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.serializer') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.annotations') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.property_info') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + ->set('cache.messenger.restart_workers_signal') + ->parent('cache.app') + ->private() + ->tag('cache.pool') + + ->set('cache.adapter.system', AdapterInterface::class) + ->abstract() + ->factory([AbstractAdapter::class, 'createSystemCache']) + ->args([ + '', // namespace + 0, // default lifetime + abstract_arg('version'), + sprintf('%s/pools/system', param('kernel.cache_dir')), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('cache.pool', ['clearer' => 'cache.system_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.apcu', ApcuAdapter::class) + ->abstract() + ->args([ + '', // namespace + 0, // default lifetime + abstract_arg('version'), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', ['clearer' => 'cache.default_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.filesystem', FilesystemAdapter::class) + ->abstract() + ->args([ + '', // namespace + 0, // default lifetime + sprintf('%s/pools/app', param('kernel.cache_dir')), + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', ['clearer' => 'cache.default_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.psr6', ProxyAdapter::class) + ->abstract() + ->args([ + abstract_arg('PSR-6 provider service'), + '', // namespace + 0, // default lifetime + ]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_psr6_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + + ->set('cache.adapter.redis', RedisAdapter::class) + ->abstract() + ->args([ + abstract_arg('Redis connection service'), + '', // namespace + 0, // default lifetime + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_redis_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.redis_tag_aware', RedisTagAwareAdapter::class) + ->abstract() + ->args([ + abstract_arg('Redis connection service'), + '', // namespace + 0, // default lifetime + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_redis_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.memcached', MemcachedAdapter::class) + ->abstract() + ->args([ + abstract_arg('Memcached connection service'), + '', // namespace + 0, // default lifetime + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_memcached_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.doctrine_dbal', DoctrineDbalAdapter::class) + ->abstract() + ->args([ + abstract_arg('DBAL connection service'), + '', // namespace + 0, // default lifetime + [], // table options + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_doctrine_dbal_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.pdo', PdoAdapter::class) + ->abstract() + ->args([ + abstract_arg('PDO connection service'), + '', // namespace + 0, // default lifetime + [], // table options + service('cache.default_marshaller')->ignoreOnInvalid(), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', [ + 'provider' => 'cache.default_pdo_provider', + 'clearer' => 'cache.default_clearer', + 'reset' => 'reset', + ]) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.adapter.array', ArrayAdapter::class) + ->abstract() + ->args([ + 0, // default lifetime + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('cache.pool', ['clearer' => 'cache.default_clearer', 'reset' => 'reset']) + ->tag('monolog.logger', ['channel' => 'cache']) + + ->set('cache.default_marshaller', DefaultMarshaller::class) + ->args([ + null, // use igbinary_serialize() when available + '%kernel.debug%', + ]) + + ->set('cache.early_expiration_handler', EarlyExpirationHandler::class) + ->args([ + service('reverse_container'), + ]) + ->tag('messenger.message_handler') + + ->set('cache.default_clearer', Psr6CacheClearer::class) + ->args([ + [], + ]) + + ->set('cache.system_clearer') + ->parent('cache.default_clearer') + ->public() + + ->set('cache.global_clearer') + ->parent('cache.default_clearer') + ->public() + + ->alias('cache.app_clearer', 'cache.default_clearer') + ->public() + + ->alias(CacheItemPoolInterface::class, 'cache.app') + + ->alias(CacheInterface::class, 'cache.app') + + ->alias(TagAwareCacheInterface::class, 'cache.app.taggable') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml deleted file mode 100644 index 41264e9d1acab..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - The "Psr\SimpleCache\CacheInterface" / "%service_id%" service is deprecated since Symfony 4.3. Use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 - - %kernel.cache_dir%/pools - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - 0 - %kernel.cache_dir%/pools - - - - - - - - - - - 0 - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - - - 0 - - - - - - - - - - - 0 - - - - - - - null - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.php new file mode 100644 index 0000000000000..82461d91a69dd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\CacheWarmer\CachePoolClearerCacheWarmer; +use Symfony\Component\Cache\DataCollector\CacheDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + // DataCollector (public to prevent inlining, made private in CacheCollectorPass) + ->set('data_collector.cache', CacheDataCollector::class) + ->public() + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/cache.html.twig', + 'id' => 'cache', + 'priority' => 275, + ]) + + // CacheWarmer used in dev to clear cache pool + ->set('cache_pool_clearer.cache_warmer', CachePoolClearerCacheWarmer::class) + ->args([ + service('cache.system_clearer'), + [ + 'cache.validator', + 'cache.serializer', + ], + ]) + ->tag('kernel.cache_warmer', ['priority' => 64]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml deleted file mode 100644 index 20e22761a308d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php new file mode 100644 index 0000000000000..abf9ded5e5949 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector; +use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector; +use Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector; +use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; +use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; +use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; +use Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector; +use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; +use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; +use Symfony\Component\HttpKernel\KernelEvents; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.config', ConfigDataCollector::class) + ->call('setKernel', [service('kernel')->ignoreOnInvalid()]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/config.html.twig', 'id' => 'config', 'priority' => -255]) + + ->set('data_collector.request', RequestDataCollector::class) + ->args([ + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335]) + + ->set('data_collector.request.session_collector', \Closure::class) + ->factory([\Closure::class, 'fromCallable']) + ->args([[service('data_collector.request'), 'collectSessionUsage']]) + + ->set('data_collector.ajax', AjaxDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/ajax.html.twig', 'id' => 'ajax', 'priority' => 315]) + + ->set('data_collector.exception', ExceptionDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/exception.html.twig', 'id' => 'exception', 'priority' => 305]) + + ->set('data_collector.events', EventDataCollector::class) + ->args([ + service('debug.event_dispatcher')->ignoreOnInvalid(), + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/events.html.twig', 'id' => 'events', 'priority' => 290]) + + ->set('data_collector.logger', LoggerDataCollector::class) + ->args([ + service('logger')->ignoreOnInvalid(), + sprintf('%s/%s', param('kernel.build_dir'), param('kernel.container_class')), + service('request_stack')->ignoreOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'profiler']) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/logger.html.twig', 'id' => 'logger', 'priority' => 300]) + + ->set('data_collector.time', TimeDataCollector::class) + ->args([ + service('kernel')->ignoreOnInvalid(), + service('debug.stopwatch')->ignoreOnInvalid(), + ]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/time.html.twig', 'id' => 'time', 'priority' => 330]) + + ->set('data_collector.memory', MemoryDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/memory.html.twig', 'id' => 'memory', 'priority' => 325]) + + ->set('data_collector.router', RouterDataCollector::class) + ->tag('kernel.event_listener', ['event' => KernelEvents::CONTROLLER, 'method' => 'onKernelController']) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/router.html.twig', 'id' => 'router', 'priority' => 285]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml deleted file mode 100644 index 17df61db1c13a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.cache_dir%/%kernel.container_class% - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php new file mode 100644 index 0000000000000..610a83addec42 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -0,0 +1,331 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\Command\AboutCommand; +use Symfony\Bundle\FrameworkBundle\Command\AssetsInstallCommand; +use Symfony\Bundle\FrameworkBundle\Command\CacheClearCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolListCommand; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand; +use Symfony\Bundle\FrameworkBundle\Command\CacheWarmupCommand; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDumpReferenceCommand; +use Symfony\Bundle\FrameworkBundle\Command\ContainerDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\ContainerLintCommand; +use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand; +use Symfony\Bundle\FrameworkBundle\Command\EventDispatcherDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsDecryptToLocalCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsEncryptFromLocalCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeysCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsRemoveCommand; +use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; +use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; +use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; +use Symfony\Component\Console\EventListener\ErrorListener; +use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand; +use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; +use Symfony\Component\Messenger\Command\DebugCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; +use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; +use Symfony\Component\Messenger\Command\SetupTransportsCommand; +use Symfony\Component\Messenger\Command\StopWorkersCommand; +use Symfony\Component\Translation\Command\TranslationPullCommand; +use Symfony\Component\Translation\Command\TranslationPushCommand; +use Symfony\Component\Translation\Command\XliffLintCommand; +use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('console.error_listener', ErrorListener::class) + ->args([ + service('logger')->nullOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'console']) + + ->set('console.suggest_missing_package_subscriber', SuggestMissingPackageSubscriber::class) + ->tag('kernel.event_subscriber') + + ->set('console.command.about', AboutCommand::class) + ->tag('console.command') + + ->set('console.command.assets_install', AssetsInstallCommand::class) + ->args([ + service('filesystem'), + param('kernel.project_dir'), + ]) + ->tag('console.command') + + ->set('console.command.cache_clear', CacheClearCommand::class) + ->args([ + service('cache_clearer'), + service('filesystem'), + ]) + ->tag('console.command') + + ->set('console.command.cache_pool_clear', CachePoolClearCommand::class) + ->args([ + service('cache.global_clearer'), + ]) + ->tag('console.command') + + ->set('console.command.cache_pool_prune', CachePoolPruneCommand::class) + ->args([ + [], + ]) + ->tag('console.command') + + ->set('console.command.cache_pool_delete', CachePoolDeleteCommand::class) + ->args([ + service('cache.global_clearer'), + ]) + ->tag('console.command') + + ->set('console.command.cache_pool_list', CachePoolListCommand::class) + ->args([ + null, + ]) + ->tag('console.command') + + ->set('console.command.cache_warmup', CacheWarmupCommand::class) + ->args([ + service('cache_warmer'), + ]) + ->tag('console.command') + + ->set('console.command.config_debug', ConfigDebugCommand::class) + ->tag('console.command') + + ->set('console.command.config_dump_reference', ConfigDumpReferenceCommand::class) + ->tag('console.command') + + ->set('console.command.container_debug', ContainerDebugCommand::class) + ->tag('console.command') + + ->set('console.command.container_lint', ContainerLintCommand::class) + ->tag('console.command') + + ->set('console.command.debug_autowiring', DebugAutowiringCommand::class) + ->args([ + null, + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.dotenv_debug', DotenvDebugCommand::class) + ->args([ + param('kernel.environment'), + param('kernel.project_dir'), + ]) + ->tag('console.command') + + ->set('console.command.event_dispatcher_debug', EventDispatcherDebugCommand::class) + ->args([ + tagged_locator('event_dispatcher.dispatcher', 'name'), + ]) + ->tag('console.command') + + ->set('console.command.messenger_consume_messages', ConsumeMessagesCommand::class) + ->args([ + abstract_arg('Routable message bus'), + service('messenger.receiver_locator'), + service('event_dispatcher'), + service('logger')->nullOnInvalid(), + [], // Receiver names + service('messenger.listener.reset_services')->nullOnInvalid(), + [], // Bus names + ]) + ->tag('console.command') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('console.command.messenger_setup_transports', SetupTransportsCommand::class) + ->args([ + service('messenger.receiver_locator'), + [], // Receiver names + ]) + ->tag('console.command') + + ->set('console.command.messenger_debug', DebugCommand::class) + ->args([ + [], // Message to handlers mapping + ]) + ->tag('console.command') + + ->set('console.command.messenger_stop_workers', StopWorkersCommand::class) + ->args([ + service('cache.messenger.restart_workers_signal'), + ]) + ->tag('console.command') + + ->set('console.command.messenger_failed_messages_retry', FailedMessagesRetryCommand::class) + ->args([ + abstract_arg('Default failure receiver name'), + abstract_arg('Receivers'), + service('messenger.routable_message_bus'), + service('event_dispatcher'), + service('logger'), + ]) + ->tag('console.command') + + ->set('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class) + ->args([ + abstract_arg('Default failure receiver name'), + abstract_arg('Receivers'), + ]) + ->tag('console.command') + + ->set('console.command.messenger_failed_messages_remove', FailedMessagesRemoveCommand::class) + ->args([ + abstract_arg('Default failure receiver name'), + abstract_arg('Receivers'), + ]) + ->tag('console.command') + + ->set('console.command.router_debug', RouterDebugCommand::class) + ->args([ + service('router'), + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.router_match', RouterMatchCommand::class) + ->args([ + service('router'), + tagged_iterator('routing.expression_language_provider'), + ]) + ->tag('console.command') + + ->set('console.command.translation_debug', TranslationDebugCommand::class) + ->args([ + service('translator'), + service('translation.reader'), + service('translation.extractor'), + param('translator.default_path'), + null, // twig.default_path + [], // Translator paths + [], // Twig paths + param('kernel.enabled_locales'), + ]) + ->tag('console.command') + + ->set('console.command.translation_extract', TranslationUpdateCommand::class) + ->args([ + service('translation.writer'), + service('translation.reader'), + service('translation.extractor'), + param('kernel.default_locale'), + param('translator.default_path'), + null, // twig.default_path + [], // Translator paths + [], // Twig paths + param('kernel.enabled_locales'), + ]) + ->tag('console.command') + + ->set('console.command.validator_debug', ValidatorDebugCommand::class) + ->args([ + service('validator'), + ]) + ->tag('console.command') + + ->set('console.command.translation_pull', TranslationPullCommand::class) + ->args([ + service('translation.provider_collection'), + service('translation.writer'), + service('translation.reader'), + param('kernel.default_locale'), + [], // Translator paths + [], // Enabled locales + ]) + ->tag('console.command', ['command' => 'translation:pull']) + + ->set('console.command.translation_push', TranslationPushCommand::class) + ->args([ + service('translation.provider_collection'), + service('translation.reader'), + [], // Translator paths + [], // Enabled locales + ]) + ->tag('console.command', ['command' => 'translation:push']) + + ->set('console.command.workflow_dump', WorkflowDumpCommand::class) + ->tag('console.command') + + ->set('console.command.xliff_lint', XliffLintCommand::class) + ->tag('console.command') + + ->set('console.command.yaml_lint', YamlLintCommand::class) + ->tag('console.command') + + ->set('console.command.form_debug', \Symfony\Component\Form\Command\DebugCommand::class) + ->args([ + service('form.registry'), + [], // All form types namespaces are stored here by FormPass + [], // All services form types are stored here by FormPass + [], // All type extensions are stored here by FormPass + [], // All type guessers are stored here by FormPass + service('debug.file_link_formatter')->nullOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.secrets_set', SecretsSetCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->nullOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.secrets_remove', SecretsRemoveCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->nullOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.secrets_generate_key', SecretsGenerateKeysCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.secrets_list', SecretsListCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.secrets_decrypt_to_local', SecretsDecryptToLocalCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command') + + ->set('console.command.secrets_encrypt_from_local', SecretsEncryptFromLocalCommand::class) + ->args([ + service('secrets.vault'), + service('secrets.local_vault')->ignoreOnInvalid(), + ]) + ->tag('console.command') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml deleted file mode 100644 index 7276892940acb..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - %kernel.project_dir% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %translator.default_path% - - - - - - - - - - - %kernel.default_locale% - %translator.default_path% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php new file mode 100644 index 0000000000000..cfaad8c1de241 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\NotTaggedControllerValueResolver; +use Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver; +use Symfony\Component\HttpKernel\Controller\TraceableControllerResolver; +use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('debug.event_dispatcher', TraceableEventDispatcher::class) + ->decorate('event_dispatcher') + ->args([ + service('debug.event_dispatcher.inner'), + service('debug.stopwatch'), + service('logger')->nullOnInvalid(), + service('request_stack')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'event']) + ->tag('kernel.reset', ['method' => 'reset']) + + ->set('debug.controller_resolver', TraceableControllerResolver::class) + ->decorate('controller_resolver') + ->args([ + service('debug.controller_resolver.inner'), + service('debug.stopwatch'), + ]) + + ->set('debug.argument_resolver', TraceableArgumentResolver::class) + ->decorate('argument_resolver') + ->args([ + service('debug.argument_resolver.inner'), + service('debug.stopwatch'), + ]) + + ->set('argument_resolver.not_tagged_controller', NotTaggedControllerValueResolver::class) + ->args([abstract_arg('Controller argument, set in FrameworkExtension')]) + ->tag('controller.argument_value_resolver', ['priority' => -200]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml deleted file mode 100644 index 63a61efe4bb51..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php new file mode 100644 index 0000000000000..f381b018f0629 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\HttpKernel\EventListener\DebugHandlersListener; + +return static function (ContainerConfigurator $container) { + $container->parameters()->set('debug.error_handler.throw_at', -1); + + $container->services() + ->set('debug.debug_handlers_listener', DebugHandlersListener::class) + ->args([ + null, // Exception handler + service('monolog.logger.php')->nullOnInvalid(), + null, // Log levels map for enabled error levels + param('debug.error_handler.throw_at'), + param('kernel.debug'), + param('kernel.debug'), + service('monolog.logger.deprecation')->nullOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'php']) + + ->set('debug.file_link_formatter', FileLinkFormatter::class) + ->args([param('debug.file_link_format')]) + + ->alias(FileLinkFormatter::class, 'debug.file_link_formatter') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml deleted file mode 100644 index 786158dd899e1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - -1 - - - - - - - - - null - - null - %debug.error_handler.throw_at% - %kernel.debug% - - %kernel.debug% - - - - %debug.file_link_format% - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php new file mode 100644 index 0000000000000..67f28ce44d838 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('error_handler.error_renderer.html', HtmlErrorRenderer::class) + ->args([ + inline_service() + ->factory([HtmlErrorRenderer::class, 'isDebug']) + ->args([ + service('request_stack'), + param('kernel.debug'), + ]), + param('kernel.charset'), + service('debug.file_link_formatter')->nullOnInvalid(), + param('kernel.project_dir'), + inline_service() + ->factory([HtmlErrorRenderer::class, 'getAndCleanOutputBuffer']) + ->args([service('request_stack')]), + service('logger')->nullOnInvalid(), + ]) + + ->alias('error_renderer.html', 'error_handler.error_renderer.html') + ->alias('error_renderer', 'error_renderer.html') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml deleted file mode 100644 index 4d2423feeeede..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - %kernel.debug% - - - %kernel.charset% - - %kernel.project_dir% - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.php new file mode 100644 index 0000000000000..ca0362a3e0965 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\SurrogateListener; +use Symfony\Component\HttpKernel\HttpCache\Esi; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('esi', Esi::class) + + ->set('esi_listener', SurrogateListener::class) + ->args([service('esi')->ignoreOnInvalid()]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml deleted file mode 100644 index 65e26d81e25c3..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/esi.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php new file mode 100644 index 0000000000000..75bfb7eb651af --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; +use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory; +use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\ColorType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension; +use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension; +use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; +use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension; +use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension; +use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension; +use Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension; +use Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension; +use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser; +use Symfony\Component\Form\FormFactory; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormRegistry; +use Symfony\Component\Form\FormRegistryInterface; +use Symfony\Component\Form\ResolvedFormTypeFactory; +use Symfony\Component\Form\ResolvedFormTypeFactoryInterface; +use Symfony\Component\Form\Util\ServerParams; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('form.resolved_type_factory', ResolvedFormTypeFactory::class) + + ->alias(ResolvedFormTypeFactoryInterface::class, 'form.resolved_type_factory') + + ->set('form.registry', FormRegistry::class) + ->args([ + [ + /* + * We don't need to be able to add more extensions. + * more types can be registered with the form.type tag + * more type extensions can be registered with the form.type_extension tag + * more type_guessers can be registered with the form.type_guesser tag + */ + service('form.extension'), + ], + service('form.resolved_type_factory'), + ]) + + ->alias(FormRegistryInterface::class, 'form.registry') + + ->set('form.factory', FormFactory::class) + ->args([service('form.registry')]) + + ->alias(FormFactoryInterface::class, 'form.factory') + + ->set('form.extension', DependencyInjectionExtension::class) + ->args([ + abstract_arg('All services with tag "form.type" are stored in a service locator by FormPass'), + abstract_arg('All services with tag "form.type_extension" are stored here by FormPass'), + abstract_arg('All services with tag "form.type_guesser" are stored here by FormPass'), + ]) + + ->set('form.type_guesser.validator', ValidatorTypeGuesser::class) + ->args([service('validator.mapping.class_metadata_factory')]) + ->tag('form.type_guesser') + + ->alias('form.property_accessor', 'property_accessor') + + ->set('form.choice_list_factory.default', DefaultChoiceListFactory::class) + + ->set('form.choice_list_factory.property_access', PropertyAccessDecorator::class) + ->args([ + service('form.choice_list_factory.default'), + service('form.property_accessor'), + ]) + + ->set('form.choice_list_factory.cached', CachingFactoryDecorator::class) + ->args([service('form.choice_list_factory.property_access')]) + ->tag('kernel.reset', ['method' => 'reset']) + + ->alias('form.choice_list_factory', 'form.choice_list_factory.cached') + + ->set('form.type.form', FormType::class) + ->args([service('form.property_accessor')]) + ->tag('form.type') + + ->set('form.type.choice', ChoiceType::class) + ->args([ + service('form.choice_list_factory'), + service('translator')->ignoreOnInvalid(), + ]) + ->tag('form.type') + + ->set('form.type.file', FileType::class) + ->args([service('translator')->ignoreOnInvalid()]) + ->tag('form.type') + + ->set('form.type.color', ColorType::class) + ->args([service('translator')->ignoreOnInvalid()]) + ->tag('form.type') + + ->set('form.type_extension.form.transformation_failure_handling', TransformationFailureExtension::class) + ->args([service('translator')->ignoreOnInvalid()]) + ->tag('form.type_extension', ['extended-type' => FormType::class]) + + ->set('form.type_extension.form.http_foundation', FormTypeHttpFoundationExtension::class) + ->args([service('form.type_extension.form.request_handler')]) + ->tag('form.type_extension') + + ->set('form.type_extension.form.request_handler', HttpFoundationRequestHandler::class) + ->args([service('form.server_params')]) + + ->set('form.server_params', ServerParams::class) + ->args([service('request_stack')]) + + ->set('form.type_extension.form.validator', FormTypeValidatorExtension::class) + ->args([ + service('validator'), + false, + service('twig.form.renderer')->ignoreOnInvalid(), + service('translator')->ignoreOnInvalid(), + ]) + ->tag('form.type_extension', ['extended-type' => FormType::class]) + + ->set('form.type_extension.repeated.validator', RepeatedTypeValidatorExtension::class) + ->tag('form.type_extension') + + ->set('form.type_extension.submit.validator', SubmitTypeValidatorExtension::class) + ->tag('form.type_extension', ['extended-type' => SubmitType::class]) + + ->set('form.type_extension.upload.validator', UploadValidatorExtension::class) + ->args([ + service('translator'), + param('validator.translation_domain'), + ]) + ->tag('form.type_extension') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml deleted file mode 100644 index 05a58c4c4cd2c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %validator.translation_domain% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php new file mode 100644 index 0000000000000..c8e5e973e40f9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('form.type_extension.csrf', FormTypeCsrfExtension::class) + ->args([ + service('security.csrf.token_manager'), + param('form.type_extension.csrf.enabled'), + param('form.type_extension.csrf.field_name'), + service('translator')->nullOnInvalid(), + param('validator.translation_domain'), + service('form.server_params'), + ]) + ->tag('form.type_extension') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml deleted file mode 100644 index 5e897bea8a30b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_csrf.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - %form.type_extension.csrf.enabled% - %form.type_extension.csrf.field_name% - - %validator.translation_domain% - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.php new file mode 100644 index 0000000000000..f5e2c3ecdd57e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; +use Symfony\Component\Form\Extension\DataCollector\FormDataExtractor; +use Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeFactoryDataCollectorProxy; +use Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension; +use Symfony\Component\Form\ResolvedFormTypeFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('form.resolved_type_factory', ResolvedTypeFactoryDataCollectorProxy::class) + ->args([ + inline_service(ResolvedFormTypeFactory::class), + service('data_collector.form'), + ]) + + ->set('form.type_extension.form.data_collector', DataCollectorTypeExtension::class) + ->args([service('data_collector.form')]) + ->tag('form.type_extension') + + ->set('data_collector.form.extractor', FormDataExtractor::class) + + ->set('data_collector.form', FormDataCollector::class) + ->args([service('data_collector.form.extractor')]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/form.html.twig', 'id' => 'form', 'priority' => 310]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml deleted file mode 100644 index 5e3e97aad5242..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.php new file mode 100644 index 0000000000000..465c304263dac --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\FragmentListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('fragment.listener', FragmentListener::class) + ->args([service('uri_signer'), param('fragment.path')]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.xml deleted file mode 100644 index b7c64119f88e6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_listener.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - %fragment.path% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php new file mode 100644 index 0000000000000..76f49c6339444 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler; +use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\FragmentUriGenerator; +use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; +use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; +use Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('fragment.renderer.hinclude.global_template', null) + ->set('fragment.path', '/_fragment') + ; + + $container->services() + ->set('fragment.handler', LazyLoadingFragmentHandler::class) + ->args([ + abstract_arg('fragment renderer locator'), + service('request_stack'), + param('kernel.debug'), + ]) + + ->set('fragment.uri_generator', FragmentUriGenerator::class) + ->args([param('fragment.path'), service('uri_signer'), service('request_stack')]) + ->alias(FragmentUriGeneratorInterface::class, 'fragment.uri_generator') + + ->set('fragment.renderer.inline', InlineFragmentRenderer::class) + ->args([service('http_kernel'), service('event_dispatcher')]) + ->call('setFragmentPath', [param('fragment.path')]) + ->tag('kernel.fragment_renderer', ['alias' => 'inline']) + + ->set('fragment.renderer.hinclude', HIncludeFragmentRenderer::class) + ->args([ + service('twig')->nullOnInvalid(), + service('uri_signer'), + param('fragment.renderer.hinclude.global_template'), + ]) + ->call('setFragmentPath', [param('fragment.path')]) + + ->set('fragment.renderer.esi', EsiFragmentRenderer::class) + ->args([ + service('esi')->nullOnInvalid(), + service('fragment.renderer.inline'), + service('uri_signer'), + ]) + ->call('setFragmentPath', [param('fragment.path')]) + ->tag('kernel.fragment_renderer', ['alias' => 'esi']) + + ->set('fragment.renderer.ssi', SsiFragmentRenderer::class) + ->args([ + service('ssi')->nullOnInvalid(), + service('fragment.renderer.inline'), + service('uri_signer'), + ]) + ->call('setFragmentPath', [param('fragment.path')]) + ->tag('kernel.fragment_renderer', ['alias' => 'ssi']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml deleted file mode 100644 index 394033734d2d4..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/fragment_renderer.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - /_fragment - - - - - - - - - %kernel.debug% - - - - - - - %fragment.path% - - - - - - %fragment.renderer.hinclude.global_template% - %fragment.path% - - - - - - - - %fragment.path% - - - - - - - - %fragment.path% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php new file mode 100644 index 0000000000000..ba70b90ad654b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpClient\HttplugClient; +use Symfony\Component\HttpClient\Psr18Client; +use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('http_client', HttpClientInterface::class) + ->factory([HttpClient::class, 'create']) + ->args([ + [], // default options + abstract_arg('max host connections'), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'http_client']) + ->tag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']) + ->tag('http_client.client') + + ->alias(HttpClientInterface::class, 'http_client') + + ->set('psr18.http_client', Psr18Client::class) + ->args([ + service('http_client'), + service(ResponseFactoryInterface::class)->ignoreOnInvalid(), + service(StreamFactoryInterface::class)->ignoreOnInvalid(), + ]) + + ->alias(ClientInterface::class, 'psr18.http_client') + + ->set(\Http\Client\HttpClient::class, HttplugClient::class) + ->args([ + service('http_client'), + service(ResponseFactoryInterface::class)->ignoreOnInvalid(), + service(StreamFactoryInterface::class)->ignoreOnInvalid(), + ]) + + ->set('http_client.abstract_retry_strategy', GenericRetryStrategy::class) + ->abstract() + ->args([ + abstract_arg('http codes'), + abstract_arg('delay ms'), + abstract_arg('multiplier'), + abstract_arg('max delay ms'), + abstract_arg('jitter'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml deleted file mode 100644 index 10256b69d5e96..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.php new file mode 100644 index 0000000000000..44031eb5f8e52 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.http_client', HttpClientDataCollector::class) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/http_client.html.twig', + 'id' => 'http_client', + 'priority' => 250, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml deleted file mode 100644 index 6d6ae4b729093..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.php new file mode 100644 index 0000000000000..5b88899153e1e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Contracts\Translation\TranslatorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translator', IdentityTranslator::class) + ->alias(TranslatorInterface::class, 'translator') + + ->set('identity_translator', IdentityTranslator::class) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml deleted file mode 100644 index 4d2d488edb354..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.2, use "identity_translator" instead. - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.php new file mode 100644 index 0000000000000..4e14636211c2d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\Store\CombinedStore; +use Symfony\Component\Lock\Strategy\ConsensusStrategy; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('lock.store.combined.abstract', CombinedStore::class)->abstract() + ->args([abstract_arg('List of stores'), service('lock.strategy.majority')]) + + ->set('lock.strategy.majority', ConsensusStrategy::class) + + ->set('lock.factory.abstract', LockFactory::class)->abstract() + ->args([abstract_arg('Store')]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'lock']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml deleted file mode 100644 index a82003c004a15..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. - - - - The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. - - - - - The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. - - - - - The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php new file mode 100644 index 0000000000000..51ad286273e06 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mailer\EventListener\EnvelopeListener; +use Symfony\Component\Mailer\EventListener\MessageListener; +use Symfony\Component\Mailer\EventListener\MessageLoggerListener; +use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Messenger\MessageHandler; +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mailer\Transport\Transports; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mailer.mailer', Mailer::class) + ->args([ + service('mailer.transports'), + abstract_arg('message bus'), + service('event_dispatcher')->ignoreOnInvalid(), + ]) + ->alias('mailer', 'mailer.mailer') + ->alias(MailerInterface::class, 'mailer.mailer') + + ->set('mailer.transports', Transports::class) + ->factory([service('mailer.transport_factory'), 'fromStrings']) + ->args([ + abstract_arg('transports'), + ]) + + ->set('mailer.transport_factory', Transport::class) + ->args([ + tagged_iterator('mailer.transport_factory'), + ]) + + ->set('mailer.default_transport', TransportInterface::class) + ->factory([service('mailer.transport_factory'), 'fromString']) + ->args([ + abstract_arg('env(MAILER_DSN)'), + ]) + ->alias(TransportInterface::class, 'mailer.default_transport') + + ->set('mailer.messenger.message_handler', MessageHandler::class) + ->args([ + service('mailer.transports'), + ]) + ->tag('messenger.message_handler') + + ->set('mailer.envelope_listener', EnvelopeListener::class) + ->args([ + abstract_arg('sender'), + abstract_arg('recipients'), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.message_listener', MessageListener::class) + ->args([ + abstract_arg('headers'), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.message_logger_listener', MessageLoggerListener::class) + ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => 'reset']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml deleted file mode 100644 index 12d40229932cd..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.php new file mode 100644 index 0000000000000..cdb205750f05d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mailer\DataCollector\MessageDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mailer.data_collector', MessageDataCollector::class) + ->args([ + service('mailer.message_logger_listener'), + ]) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/mailer.html.twig', + 'id' => 'mailer', + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml deleted file mode 100644 index 17e1a6ed54ad9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php new file mode 100644 index 0000000000000..7bddfa7567cee --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\NativeTransportFactory; +use Symfony\Component\Mailer\Transport\NullTransportFactory; +use Symfony\Component\Mailer\Transport\SendmailTransportFactory; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mailer.transport_factory.abstract', AbstractTransportFactory::class) + ->abstract() + ->args([ + service('event_dispatcher'), + service('http_client')->ignoreOnInvalid(), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'mailer']) + + ->set('mailer.transport_factory.amazon', SesTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.gmail', GmailTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.mailchimp', MandrillTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.mailjet', MailjetTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.mailgun', MailgunTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.postmark', PostmarkTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.sendgrid', SendgridTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.null', NullTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.sendmail', SendmailTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.sendinblue', SendinblueTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.ohmysmtp', OhMySmtpTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + + ->set('mailer.transport_factory.smtp', EsmtpTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory', ['priority' => -100]) + + ->set('mailer.transport_factory.native', NativeTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory'); +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml deleted file mode 100644 index d478942a0c3f0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php new file mode 100644 index 0000000000000..813d503000de4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; +use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; +use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; +use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; +use Symfony\Component\Messenger\EventListener\AddErrorDetailsStampListener; +use Symfony\Component\Messenger\EventListener\DispatchPcntlSignalListener; +use Symfony\Component\Messenger\EventListener\ResetServicesListener; +use Symfony\Component\Messenger\EventListener\SendFailedMessageForRetryListener; +use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnCustomStopExceptionListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; +use Symfony\Component\Messenger\EventListener\StopWorkerOnSigtermSignalListener; +use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware; +use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware; +use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware; +use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; +use Symfony\Component\Messenger\Middleware\RejectRedeliveredMessageMiddleware; +use Symfony\Component\Messenger\Middleware\RouterContextMiddleware; +use Symfony\Component\Messenger\Middleware\SendMessageMiddleware; +use Symfony\Component\Messenger\Middleware\TraceableMiddleware; +use Symfony\Component\Messenger\Middleware\ValidationMiddleware; +use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy; +use Symfony\Component\Messenger\RoutableMessageBus; +use Symfony\Component\Messenger\Transport\InMemoryTransportFactory; +use Symfony\Component\Messenger\Transport\Sender\SendersLocator; +use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer; +use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; +use Symfony\Component\Messenger\Transport\Serialization\Serializer; +use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; +use Symfony\Component\Messenger\Transport\Sync\SyncTransportFactory; +use Symfony\Component\Messenger\Transport\TransportFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->alias(SerializerInterface::class, 'messenger.default_serializer') + + // Asynchronous + ->set('messenger.senders_locator', SendersLocator::class) + ->args([ + abstract_arg('per message senders map'), + abstract_arg('senders service locator'), + ]) + ->set('messenger.middleware.send_message', SendMessageMiddleware::class) + ->args([ + service('messenger.senders_locator'), + service('event_dispatcher'), + ]) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'messenger']) + + // Message encoding/decoding + ->set('messenger.transport.symfony_serializer', Serializer::class) + ->args([ + service('serializer'), + abstract_arg('format'), + abstract_arg('context'), + ]) + + ->set('serializer.normalizer.flatten_exception', FlattenExceptionNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -880]) + + ->set('messenger.transport.native_php_serializer', PhpSerializer::class) + + // Middleware + ->set('messenger.middleware.handle_message', HandleMessageMiddleware::class) + ->abstract() + ->args([ + abstract_arg('bus handler resolver'), + ]) + ->tag('monolog.logger', ['channel' => 'messenger']) + ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + + ->set('messenger.middleware.add_bus_name_stamp_middleware', AddBusNameStampMiddleware::class) + ->abstract() + + ->set('messenger.middleware.dispatch_after_current_bus', DispatchAfterCurrentBusMiddleware::class) + + ->set('messenger.middleware.validation', ValidationMiddleware::class) + ->args([ + service('validator'), + ]) + + ->set('messenger.middleware.reject_redelivered_message_middleware', RejectRedeliveredMessageMiddleware::class) + + ->set('messenger.middleware.failed_message_processing_middleware', FailedMessageProcessingMiddleware::class) + + ->set('messenger.middleware.traceable', TraceableMiddleware::class) + ->abstract() + ->args([ + service('debug.stopwatch'), + ]) + + ->set('messenger.middleware.router_context', RouterContextMiddleware::class) + ->args([ + service('router'), + ]) + + // Discovery + ->set('messenger.receiver_locator', ServiceLocator::class) + ->args([ + [], + ]) + ->tag('container.service_locator') + + // Transports + ->set('messenger.transport_factory', TransportFactory::class) + ->args([ + tagged_iterator('messenger.transport_factory'), + ]) + + ->set('messenger.transport.amqp.factory', AmqpTransportFactory::class) + + ->set('messenger.transport.redis.factory', RedisTransportFactory::class) + + ->set('messenger.transport.sync.factory', SyncTransportFactory::class) + ->args([ + service('messenger.routable_message_bus'), + ]) + ->tag('messenger.transport_factory') + + ->set('messenger.transport.in_memory.factory', InMemoryTransportFactory::class) + ->tag('messenger.transport_factory') + ->tag('kernel.reset', ['method' => 'reset']) + + ->set('messenger.transport.sqs.factory', AmazonSqsTransportFactory::class) + ->args([ + service('logger')->ignoreOnInvalid(), + ]) + + ->set('messenger.transport.beanstalkd.factory', BeanstalkdTransportFactory::class) + + // retry + ->set('messenger.retry_strategy_locator', ServiceLocator::class) + ->args([ + [], + ]) + ->tag('container.service_locator') + + ->set('messenger.retry.abstract_multiplier_retry_strategy', MultiplierRetryStrategy::class) + ->abstract() + ->args([ + abstract_arg('max retries'), + abstract_arg('delay ms'), + abstract_arg('multiplier'), + abstract_arg('max delay ms'), + ]) + + // worker event listener + ->set('messenger.retry.send_failed_message_for_retry_listener', SendFailedMessageForRetryListener::class) + ->args([ + abstract_arg('senders service locator'), + service('messenger.retry_strategy_locator'), + service('logger')->ignoreOnInvalid(), + service('event_dispatcher'), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('messenger.failure.add_error_details_stamp_listener', AddErrorDetailsStampListener::class) + ->tag('kernel.event_subscriber') + + ->set('messenger.failure.send_failed_message_to_failure_transport_listener', SendFailedMessageToFailureTransportListener::class) + ->args([ + abstract_arg('failure transports'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('messenger.listener.dispatch_pcntl_signal_listener', DispatchPcntlSignalListener::class) + ->tag('kernel.event_subscriber') + + ->set('messenger.listener.stop_worker_on_restart_signal_listener', StopWorkerOnRestartSignalListener::class) + ->args([ + service('cache.messenger.restart_workers_signal'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'messenger']) + + ->set('messenger.listener.stop_worker_on_sigterm_signal_listener', StopWorkerOnSigtermSignalListener::class) + ->args([ + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.event_subscriber') + + ->set('messenger.listener.stop_worker_on_stop_exception_listener', StopWorkerOnCustomStopExceptionListener::class) + ->tag('kernel.event_subscriber') + + ->set('messenger.listener.reset_services', ResetServicesListener::class) + ->args([ + service('services_resetter'), + ]) + + ->set('messenger.routable_message_bus', RoutableMessageBus::class) + ->args([ + abstract_arg('message bus locator'), + service('messenger.default_bus'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml deleted file mode 100644 index 839aba901b915..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.php new file mode 100644 index 0000000000000..58f9be1f9e531 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Messenger\DataCollector\MessengerDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.messenger', MessengerDataCollector::class) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/messenger.html.twig', + 'id' => 'messenger', + 'priority' => 100, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.xml deleted file mode 100644 index 96f43b3b33d79..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger_debug.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.php new file mode 100644 index 0000000000000..a7e9bbd912746 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Mime\MimeTypeGuesserInterface; +use Symfony\Component\Mime\MimeTypes; +use Symfony\Component\Mime\MimeTypesInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('mime_types', MimeTypes::class) + ->call('setDefault', [service('mime_types')]) + + ->alias(MimeTypesInterface::class, 'mime_types') + ->alias(MimeTypeGuesserInterface::class, 'mime_types') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml deleted file mode 100644 index e91705d1c19ed..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php new file mode 100644 index 0000000000000..73beb2c346698 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Monolog\Handler\NotifierHandler; +use Symfony\Component\Notifier\Channel\BrowserChannel; +use Symfony\Component\Notifier\Channel\ChannelPolicy; +use Symfony\Component\Notifier\Channel\ChatChannel; +use Symfony\Component\Notifier\Channel\EmailChannel; +use Symfony\Component\Notifier\Channel\PushChannel; +use Symfony\Component\Notifier\Channel\SmsChannel; +use Symfony\Component\Notifier\Chatter; +use Symfony\Component\Notifier\ChatterInterface; +use Symfony\Component\Notifier\EventListener\NotificationLoggerListener; +use Symfony\Component\Notifier\EventListener\SendFailedMessageToNotifierListener; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Messenger\MessageHandler; +use Symfony\Component\Notifier\Notifier; +use Symfony\Component\Notifier\NotifierInterface; +use Symfony\Component\Notifier\Texter; +use Symfony\Component\Notifier\TexterInterface; +use Symfony\Component\Notifier\Transport; +use Symfony\Component\Notifier\Transport\Transports; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('notifier', Notifier::class) + ->args([tagged_locator('notifier.channel', 'channel'), service('notifier.channel_policy')->ignoreOnInvalid()]) + + ->alias(NotifierInterface::class, 'notifier') + + ->set('notifier.channel_policy', ChannelPolicy::class) + ->args([[]]) + + ->set('notifier.channel.browser', BrowserChannel::class) + ->args([service('request_stack')]) + ->tag('notifier.channel', ['channel' => 'browser']) + + ->set('notifier.channel.chat', ChatChannel::class) + ->args([service('chatter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'chat']) + + ->set('notifier.channel.sms', SmsChannel::class) + ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'sms']) + + ->set('notifier.channel.email', EmailChannel::class) + ->args([service('mailer.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'email']) + + ->set('notifier.channel.push', PushChannel::class) + ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'push']) + + ->set('notifier.monolog_handler', NotifierHandler::class) + ->args([service('notifier')]) + + ->set('notifier.failed_message_listener', SendFailedMessageToNotifierListener::class) + ->args([service('notifier')]) + + ->set('chatter', Chatter::class) + ->args([ + service('chatter.transports'), + service('messenger.default_bus')->ignoreOnInvalid(), + service('event_dispatcher')->ignoreOnInvalid(), + ]) + + ->alias(ChatterInterface::class, 'chatter') + + ->set('chatter.transports', Transports::class) + ->factory([service('chatter.transport_factory'), 'fromStrings']) + ->args([[]]) + + ->set('chatter.transport_factory', Transport::class) + ->args([tagged_iterator('chatter.transport_factory')]) + + ->set('chatter.messenger.chat_handler', MessageHandler::class) + ->args([service('chatter.transports')]) + ->tag('messenger.message_handler', ['handles' => ChatMessage::class]) + + ->set('texter', Texter::class) + ->args([ + service('texter.transports'), + service('messenger.default_bus')->ignoreOnInvalid(), + service('event_dispatcher')->ignoreOnInvalid(), + ]) + + ->alias(TexterInterface::class, 'texter') + + ->set('texter.transports', Transports::class) + ->factory([service('texter.transport_factory'), 'fromStrings']) + ->args([[]]) + + ->set('texter.transport_factory', Transport::class) + ->args([tagged_iterator('texter.transport_factory')]) + + ->set('texter.messenger.sms_handler', MessageHandler::class) + ->args([service('texter.transports')]) + ->tag('messenger.message_handler', ['handles' => SmsMessage::class]) + + ->set('texter.messenger.push_handler', MessageHandler::class) + ->args([service('texter.transports')]) + ->tag('messenger.message_handler', ['handles' => PushMessage::class]) + + ->set('notifier.logger_notification_listener', NotificationLoggerListener::class) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_debug.php new file mode 100644 index 0000000000000..6147d34e4e7eb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_debug.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Notifier\DataCollector\NotificationDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('notifier.data_collector', NotificationDataCollector::class) + ->args([service('notifier.logger_notification_listener')]) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/notifier.html.twig', 'id' => 'notifier']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php new file mode 100644 index 0000000000000..1af10c4fbf6f4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsTransportFactory; +use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; +use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; +use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; +use Symfony\Component\Notifier\Bridge\Expo\ExpoTransportFactory; +use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; +use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; +use Symfony\Component\Notifier\Bridge\GatewayApi\GatewayApiTransportFactory; +use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; +use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; +use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory; +use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; +use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory; +use Symfony\Component\Notifier\Bridge\Mailjet\MailjetTransportFactory; +use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; +use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; +use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; +use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; +use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; +use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; +use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; +use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; +use Symfony\Component\Notifier\Bridge\Sinch\SinchTransportFactory; +use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; +use Symfony\Component\Notifier\Bridge\Sms77\Sms77TransportFactory; +use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransportFactory; +use Symfony\Component\Notifier\Bridge\SmsBiuras\SmsBiurasTransportFactory; +use Symfony\Component\Notifier\Bridge\Smsc\SmscTransportFactory; +use Symfony\Component\Notifier\Bridge\SpotHit\SpotHitTransportFactory; +use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; +use Symfony\Component\Notifier\Bridge\Telnyx\TelnyxTransportFactory; +use Symfony\Component\Notifier\Bridge\TurboSms\TurboSmsTransportFactory; +use Symfony\Component\Notifier\Bridge\Twilio\TwilioTransportFactory; +use Symfony\Component\Notifier\Bridge\Vonage\VonageTransportFactory; +use Symfony\Component\Notifier\Bridge\Yunpian\YunpianTransportFactory; +use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\NullTransportFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + + ->set('notifier.transport_factory.abstract', AbstractTransportFactory::class) + ->abstract() + ->args([service('event_dispatcher'), service('http_client')->ignoreOnInvalid()]) + + ->set('notifier.transport_factory.slack', SlackTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.linked-in', LinkedInTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.telegram', TelegramTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.mattermost', MattermostTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.vonage', VonageTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.rocket-chat', RocketChatTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.google-chat', GoogleChatTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.twilio', TwilioTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.all-my-sms', AllMySmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.firebase', FirebaseTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.free-mobile', FreeMobileTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.spot-hit', SpotHitTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.fake-chat', FakeChatTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.fake-sms', FakeSmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.ovh-cloud', OvhCloudTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sinch', SinchTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.zulip', ZulipTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.infobip', InfobipTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.mobyt', MobytTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.smsapi', SmsapiTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.esendex', EsendexTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sendinblue', SendinblueTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.iqsms', IqsmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.octopush', OctopushTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.discord', DiscordTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.microsoft-teams', MicrosoftTeamsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.gateway-api', GatewayApiTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.mercure', MercureTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.gitter', GitterTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.clickatell', ClickatellTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.amazon-sns', AmazonSnsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->tag('chatter.transport_factory') + + ->set('notifier.transport_factory.null', NullTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('chatter.transport_factory') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.light-sms', LightSmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sms-biuras', SmsBiurasTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.smsc', SmscTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.message-bird', MessageBirdTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.message-media', MessageMediaTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.telnyx', TelnyxTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.mailjet', MailjetTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.yunpian', YunpianTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.turbo-sms', TurboSmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.sms77', Sms77TransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.one-signal', OneSignalTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.expo', ExpoTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php new file mode 100644 index 0000000000000..221217896fe93 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\ProfilerListener; +use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; +use Symfony\Component\HttpKernel\Profiler\Profiler; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('profiler', Profiler::class) + ->public() + ->args([service('profiler.storage'), service('logger')->nullOnInvalid()]) + ->tag('monolog.logger', ['channel' => 'profiler']) + ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.4']) + + ->set('profiler.storage', FileProfilerStorage::class) + ->args([param('profiler.storage.dsn')]) + + ->set('profiler_listener', ProfilerListener::class) + ->args([ + service('profiler'), + service('request_stack'), + null, + param('profiler_listener.only_exceptions'), + param('profiler_listener.only_main_requests'), + ]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml deleted file mode 100644 index 166be86b2e203..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - %profiler.storage.dsn% - - - - - - - null - %profiler_listener.only_exceptions% - %profiler_listener.only_master_requests% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php new file mode 100644 index 0000000000000..85ab9f18e6e3b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('property_accessor', PropertyAccessor::class) + ->args([ + abstract_arg('magic methods allowed, set by the extension'), + abstract_arg('throw exceptions, set by the extension'), + service('cache.property_access')->ignoreOnInvalid(), + abstract_arg('propertyReadInfoExtractor, set by the extension'), + abstract_arg('propertyWriteInfoExtractor, set by the extension'), + ]) + + ->alias(PropertyAccessorInterface::class, 'property_accessor') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml deleted file mode 100644 index 424f9f682d796..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_access.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php new file mode 100644 index 0000000000000..90587839d54c4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('property_info', PropertyInfoExtractor::class) + ->args([[], [], [], [], []]) + + ->alias(PropertyAccessExtractorInterface::class, 'property_info') + ->alias(PropertyDescriptionExtractorInterface::class, 'property_info') + ->alias(PropertyInfoExtractorInterface::class, 'property_info') + ->alias(PropertyTypeExtractorInterface::class, 'property_info') + ->alias(PropertyListExtractorInterface::class, 'property_info') + ->alias(PropertyInitializableExtractorInterface::class, 'property_info') + + ->set('property_info.cache', PropertyInfoCacheExtractor::class) + ->decorate('property_info') + ->args([service('property_info.cache.inner'), service('cache.property_info')]) + + // Extractor + ->set('property_info.reflection_extractor', ReflectionExtractor::class) + ->tag('property_info.list_extractor', ['priority' => -1000]) + ->tag('property_info.type_extractor', ['priority' => -1002]) + ->tag('property_info.access_extractor', ['priority' => -1000]) + ->tag('property_info.initializable_extractor', ['priority' => -1000]) + + ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') + ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml deleted file mode 100644 index cd78d7f95ea56..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/property_info.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php new file mode 100644 index 0000000000000..727a1f6364456 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\RateLimiter\RateLimiterFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('cache.rate_limiter') + ->parent('cache.app') + ->tag('cache.pool') + + ->set('limiter', RateLimiterFactory::class) + ->abstract() + ->args([ + abstract_arg('config'), + abstract_arg('storage'), + null, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.php new file mode 100644 index 0000000000000..ef8fc9a5e7d8c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\AddRequestFormatsListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('request.add_request_formats_listener', AddRequestFormatsListener::class) + ->args([abstract_arg('formats')]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml deleted file mode 100644 index 048b61ec466f0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/request.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php new file mode 100644 index 0000000000000..09e340ff8aedd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\RouterCacheWarmer; +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; +use Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader; +use Symfony\Bundle\FrameworkBundle\Routing\RedirectableCompiledUrlMatcher; +use Symfony\Bundle\FrameworkBundle\Routing\Router; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\HttpKernel\EventListener\RouterListener; +use Symfony\Component\Routing\Generator\CompiledUrlGenerator; +use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Loader\ContainerLoader; +use Symfony\Component\Routing\Loader\DirectoryLoader; +use Symfony\Component\Routing\Loader\GlobFileLoader; +use Symfony\Component\Routing\Loader\PhpFileLoader; +use Symfony\Component\Routing\Loader\XmlFileLoader; +use Symfony\Component\Routing\Loader\YamlFileLoader; +use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; +use Symfony\Component\Routing\Matcher\ExpressionLanguageProvider; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; +use Symfony\Component\Routing\RouterInterface; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('router.request_context.host', 'localhost') + ->set('router.request_context.scheme', 'http') + ->set('router.request_context.base_url', '') + ; + + $container->services() + ->set('routing.resolver', LoaderResolver::class) + + ->set('routing.loader.xml', XmlFileLoader::class) + ->args([ + service('file_locator'), + '%kernel.environment%', + ]) + ->tag('routing.loader') + + ->set('routing.loader.yml', YamlFileLoader::class) + ->args([ + service('file_locator'), + '%kernel.environment%', + ]) + ->tag('routing.loader') + + ->set('routing.loader.php', PhpFileLoader::class) + ->args([ + service('file_locator'), + '%kernel.environment%', + ]) + ->tag('routing.loader') + + ->set('routing.loader.glob', GlobFileLoader::class) + ->args([ + service('file_locator'), + '%kernel.environment%', + ]) + ->tag('routing.loader') + + ->set('routing.loader.directory', DirectoryLoader::class) + ->args([ + service('file_locator'), + '%kernel.environment%', + ]) + ->tag('routing.loader') + + ->set('routing.loader.container', ContainerLoader::class) + ->args([ + tagged_locator('routing.route_loader'), + '%kernel.environment%', + ]) + ->tag('routing.loader') + + ->set('routing.loader', DelegatingLoader::class) + ->public() + ->args([ + service('routing.resolver'), + [], // Default options + [], // Default requirements + ]) + + ->set('router.default', Router::class) + ->args([ + service(ContainerInterface::class), + param('router.resource'), + [ + 'cache_dir' => param('kernel.cache_dir'), + 'debug' => param('kernel.debug'), + 'generator_class' => CompiledUrlGenerator::class, + 'generator_dumper_class' => CompiledUrlGeneratorDumper::class, + 'matcher_class' => RedirectableCompiledUrlMatcher::class, + 'matcher_dumper_class' => CompiledUrlMatcherDumper::class, + ], + service('router.request_context')->ignoreOnInvalid(), + service('parameter_bag')->ignoreOnInvalid(), + service('logger')->ignoreOnInvalid(), + param('kernel.default_locale'), + ]) + ->call('setConfigCacheFactory', [ + service('config_cache_factory'), + ]) + ->tag('monolog.logger', ['channel' => 'router']) + ->tag('container.service_subscriber', ['id' => 'routing.loader']) + ->alias('router', 'router.default') + ->public() + ->alias(RouterInterface::class, 'router') + ->alias(UrlGeneratorInterface::class, 'router') + ->alias(UrlMatcherInterface::class, 'router') + ->alias(RequestContextAwareInterface::class, 'router') + + ->set('router.request_context', RequestContext::class) + ->factory([RequestContext::class, 'fromUri']) + ->args([ + param('router.request_context.base_url'), + param('router.request_context.host'), + param('router.request_context.scheme'), + param('request_listener.http_port'), + param('request_listener.https_port'), + ]) + ->call('setParameter', [ + '_functions', + service('router.expression_language_provider')->ignoreOnInvalid(), + ]) + ->alias(RequestContext::class, 'router.request_context') + + ->set('router.expression_language_provider', ExpressionLanguageProvider::class) + ->args([ + tagged_locator('routing.expression_language_function', 'function'), + ]) + ->tag('routing.expression_language_provider') + + ->set('router.cache_warmer', RouterCacheWarmer::class) + ->args([service(ContainerInterface::class)]) + ->tag('container.service_subscriber', ['id' => 'router']) + ->tag('kernel.cache_warmer') + + ->set('router_listener', RouterListener::class) + ->args([ + service('router'), + service('request_stack'), + service('router.request_context')->ignoreOnInvalid(), + service('logger')->ignoreOnInvalid(), + param('kernel.project_dir'), + param('kernel.debug'), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'request']) + + ->set(RedirectController::class) + ->public() + ->args([ + service('router'), + inline_service('int') + ->factory([service('router.request_context'), 'getHttpPort']), + inline_service('int') + ->factory([service('router.request_context'), 'getHttpsPort']), + ]) + + ->set(TemplateController::class) + ->args([ + service('twig')->ignoreOnInvalid(), + ]) + ->public() + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml deleted file mode 100644 index b85e9fa71d1cc..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - localhost - http - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.4, use "routing.loader.container" instead. - - - - - - - - - - - - - - - - - - - - - - - %router.resource% - - %kernel.cache_dir% - %kernel.debug% - Symfony\Component\Routing\Generator\CompiledUrlGenerator - Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper - Symfony\Bundle\FrameworkBundle\Routing\RedirectableCompiledUrlMatcher - Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper - - - - - %kernel.default_locale% - - - - - - - - - - - - - %router.request_context.base_url% - GET - %router.request_context.host% - %router.request_context.scheme% - %request_listener.http_port% - %request_listener.https_port% - - - - - - - - - - - - - - - - - %kernel.project_dir% - %kernel.debug% - - - - - %request_listener.http_port% - %request_listener.https_port% - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index faa88efbbf7a2..d48e8ca1520f7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -20,7 +20,6 @@ - @@ -30,18 +29,29 @@ + + + + + + + + + + + @@ -49,6 +59,7 @@ + @@ -80,7 +91,9 @@ + + @@ -96,10 +109,12 @@ + + @@ -145,6 +160,7 @@ + @@ -158,30 +174,15 @@ - - - - - - - - - - - - - - - - - - + + + @@ -190,6 +191,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -236,6 +257,8 @@ + + @@ -243,6 +266,7 @@ + @@ -280,8 +304,9 @@ - + + @@ -292,9 +317,11 @@ + + @@ -308,10 +335,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -352,6 +403,18 @@ + + + + + + + + + + + + @@ -428,6 +491,7 @@ + @@ -463,6 +527,7 @@ + @@ -495,6 +560,7 @@ + @@ -502,6 +568,7 @@ + @@ -518,7 +585,6 @@ - @@ -527,6 +593,7 @@ + @@ -557,6 +624,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -573,8 +661,15 @@ + + + + + + + @@ -587,4 +682,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php new file mode 100644 index 0000000000000..a21d282702e13 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault; +use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('secrets.vault', SodiumVault::class) + ->args([ + abstract_arg('Secret dir, set in FrameworkExtension'), + service('secrets.decryption_key')->ignoreOnInvalid(), + ]) + ->tag('container.env_var_loader') + + ->set('secrets.decryption_key') + ->parent('container.env') + ->args([abstract_arg('Decryption env var, set in FrameworkExtension')]) + + ->set('secrets.local_vault', DotenvVault::class) + ->args([abstract_arg('.env file path, set in FrameworkExtension')]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml deleted file mode 100644 index 65fd1073fd46f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php new file mode 100644 index 0000000000000..bad2284bfb124 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Extension\CsrfExtension; +use Symfony\Bridge\Twig\Extension\CsrfRuntime; +use Symfony\Component\Security\Csrf\CsrfTokenManager; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface; +use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; +use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.csrf.token_generator', UriSafeTokenGenerator::class) + + ->alias(TokenGeneratorInterface::class, 'security.csrf.token_generator') + + ->set('security.csrf.token_storage', SessionTokenStorage::class) + ->args([service('request_stack')]) + + ->alias(TokenStorageInterface::class, 'security.csrf.token_storage') + + ->set('security.csrf.token_manager', CsrfTokenManager::class) + ->args([ + service('security.csrf.token_generator'), + service('security.csrf.token_storage'), + service('request_stack')->ignoreOnInvalid(), + ]) + + ->alias(CsrfTokenManagerInterface::class, 'security.csrf.token_manager') + + ->set('twig.runtime.security_csrf', CsrfRuntime::class) + ->args([service('security.csrf.token_manager')]) + ->tag('twig.runtime') + + ->set('twig.extension.security_csrf', CsrfExtension::class) + ->tag('twig.extension') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml deleted file mode 100644 index eefe6ad73601f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/security_csrf.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php new file mode 100644 index 0000000000000..5655c05a26e35 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -0,0 +1,220 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer; +use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\Encoder\DecoderInterface; +use Symfony\Component\Serializer\Encoder\EncoderInterface; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; +use Symfony\Component\Serializer\Encoder\YamlEncoder; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; +use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; +use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\FormErrorNormalizer; +use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; +use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Normalizer\UidNormalizer; +use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('serializer.mapping.cache.file', '%kernel.cache_dir%/serialization.php') + ; + + $container->services() + ->set('serializer', Serializer::class) + ->args([[], []]) + + ->alias(SerializerInterface::class, 'serializer') + ->alias(NormalizerInterface::class, 'serializer') + ->alias(DenormalizerInterface::class, 'serializer') + ->alias(EncoderInterface::class, 'serializer') + ->alias(DecoderInterface::class, 'serializer') + + ->alias('serializer.property_accessor', 'property_accessor') + + // Discriminator Map + ->set('serializer.mapping.class_discriminator_resolver', ClassDiscriminatorFromClassMetadata::class) + ->args([service('serializer.mapping.class_metadata_factory')]) + + ->alias(ClassDiscriminatorResolverInterface::class, 'serializer.mapping.class_discriminator_resolver') + + // Normalizer + ->set('serializer.normalizer.constraint_violation_list', ConstraintViolationListNormalizer::class) + ->args([[], service('serializer.name_converter.metadata_aware')]) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.mime_message', MimeMessageNormalizer::class) + ->args([service('serializer.normalizer.property')]) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.datetimezone', DateTimeZoneNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.dateinterval', DateIntervalNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.data_uri', DataUriNormalizer::class) + ->args([service('mime_types')->nullOnInvalid()]) + ->tag('serializer.normalizer', ['priority' => -920]) + + ->set('serializer.normalizer.datetime', DateTimeNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -910]) + + ->set('serializer.normalizer.json_serializable', JsonSerializableNormalizer::class) + ->args([null, null]) + ->tag('serializer.normalizer', ['priority' => -900]) + + ->set('serializer.normalizer.problem', ProblemNormalizer::class) + ->args([param('kernel.debug')]) + ->tag('serializer.normalizer', ['priority' => -890]) + + ->set('serializer.denormalizer.unwrapping', UnwrappingDenormalizer::class) + ->args([service('serializer.property_accessor')]) + ->tag('serializer.normalizer', ['priority' => 1000]) + + ->set('serializer.normalizer.uid', UidNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -890]) + + ->set('serializer.normalizer.form_error', FormErrorNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + + ->set('serializer.normalizer.object', ObjectNormalizer::class) + ->args([ + service('serializer.mapping.class_metadata_factory'), + service('serializer.name_converter.metadata_aware'), + service('serializer.property_accessor'), + service('property_info')->ignoreOnInvalid(), + service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), + null, + [], + ]) + ->tag('serializer.normalizer', ['priority' => -1000]) + + ->alias(ObjectNormalizer::class, 'serializer.normalizer.object') + + ->set('serializer.normalizer.property', PropertyNormalizer::class) + ->args([ + service('serializer.mapping.class_metadata_factory'), + service('serializer.name_converter.metadata_aware'), + service('property_info')->ignoreOnInvalid(), + service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), + null, + [], + ]) + + ->alias(PropertyNormalizer::class, 'serializer.normalizer.property') + + ->set('serializer.denormalizer.array', ArrayDenormalizer::class) + ->tag('serializer.normalizer', ['priority' => -990]) + + // Loader + ->set('serializer.mapping.chain_loader', LoaderChain::class) + ->args([[]]) + + // Class Metadata Factory + ->set('serializer.mapping.class_metadata_factory', ClassMetadataFactory::class) + ->args([service('serializer.mapping.chain_loader')]) + + ->alias(ClassMetadataFactoryInterface::class, 'serializer.mapping.class_metadata_factory') + + // Cache + ->set('serializer.mapping.cache_warmer', SerializerCacheWarmer::class) + ->args([abstract_arg('The serializer metadata loaders'), param('serializer.mapping.cache.file')]) + ->tag('kernel.cache_warmer') + + ->set('serializer.mapping.cache.symfony', CacheItemPoolInterface::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([param('serializer.mapping.cache.file'), service('cache.serializer')]) + + ->set('serializer.mapping.cache_class_metadata_factory', CacheClassMetadataFactory::class) + ->decorate('serializer.mapping.class_metadata_factory') + ->args([ + service('serializer.mapping.cache_class_metadata_factory.inner'), + service('serializer.mapping.cache.symfony'), + ]) + + // Encoders + ->set('serializer.encoder.xml', XmlEncoder::class) + ->tag('serializer.encoder') + + ->set('serializer.encoder.json', JsonEncoder::class) + ->tag('serializer.encoder') + + ->set('serializer.encoder.yaml', YamlEncoder::class) + ->args([null, null]) + ->tag('serializer.encoder') + + ->set('serializer.encoder.csv', CsvEncoder::class) + ->tag('serializer.encoder') + + // Name converter + ->set('serializer.name_converter.camel_case_to_snake_case', CamelCaseToSnakeCaseNameConverter::class) + + ->set('serializer.name_converter.metadata_aware', MetadataAwareNameConverter::class) + ->args([service('serializer.mapping.class_metadata_factory')]) + + // PropertyInfo extractor + ->set('property_info.serializer_extractor', SerializerExtractor::class) + ->args([service('serializer.mapping.class_metadata_factory')]) + ->tag('property_info.list_extractor', ['priority' => -999]) + + // ErrorRenderer integration + ->alias('error_renderer', 'error_renderer.serializer') + ->alias('error_renderer.serializer', 'error_handler.error_renderer.serializer') + + ->set('error_handler.error_renderer.serializer', SerializerErrorRenderer::class) + ->args([ + service('serializer'), + inline_service() + ->factory([SerializerErrorRenderer::class, 'getPreferredFormat']) + ->args([service('request_stack')]), + service('error_renderer.html'), + inline_service() + ->factory([HtmlErrorRenderer::class, 'isDebug']) + ->args([service('request_stack'), param('kernel.debug')]), + ]) + ; + + if (interface_exists(\BackedEnum::class)) { + $container->services() + ->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -915]) + ; + } +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml deleted file mode 100644 index c3eea28b8c2b9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - %kernel.cache_dir%/serialization.php - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - - - - - - - - - - - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - %serializer.mapping.cache.file% - - - - - - %serializer.mapping.cache.file% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php new file mode 100644 index 0000000000000..2a6e18e835e42 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -0,0 +1,214 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer; +use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; +use Symfony\Component\Config\Resource\SelfCheckingResourceChecker; +use Symfony\Component\Config\ResourceCheckerConfigCacheFactory; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker; +use Symfony\Component\DependencyInjection\EnvVarProcessor; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; +use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface as EventDispatcherInterfaceComponentAlias; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\UrlHelper; +use Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer; +use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate; +use Symfony\Component\HttpKernel\Config\FileLocator; +use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; +use Symfony\Component\HttpKernel\EventListener\LocaleAwareListener; +use Symfony\Component\HttpKernel\HttpCache\Store; +use Symfony\Component\HttpKernel\HttpCache\StoreInterface; +use Symfony\Component\HttpKernel\HttpKernel; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\UriSigner; +use Symfony\Component\Runtime\Runner\Symfony\HttpKernelRunner; +use Symfony\Component\Runtime\Runner\Symfony\ResponseRunner; +use Symfony\Component\Runtime\SymfonyRuntime; +use Symfony\Component\String\LazyString; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Component\String\Slugger\SluggerInterface; +use Symfony\Component\Workflow\WorkflowEvents; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +return static function (ContainerConfigurator $container) { + // this parameter is used at compile time in RegisterListenersPass + $container->parameters()->set('event_dispatcher.event_aliases', array_merge( + class_exists(ConsoleEvents::class) ? ConsoleEvents::ALIASES : [], + class_exists(FormEvents::class) ? FormEvents::ALIASES : [], + KernelEvents::ALIASES, + class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] + )); + + $container->services() + + ->set('parameter_bag', ContainerBag::class) + ->args([ + service('service_container'), + ]) + ->alias(ContainerBagInterface::class, 'parameter_bag') + ->alias(ParameterBagInterface::class, 'parameter_bag') + + ->set('event_dispatcher', EventDispatcher::class) + ->public() + ->tag('container.hot_path') + ->tag('event_dispatcher.dispatcher', ['name' => 'event_dispatcher']) + ->alias(EventDispatcherInterfaceComponentAlias::class, 'event_dispatcher') + ->alias(EventDispatcherInterface::class, 'event_dispatcher') + + ->set('http_kernel', HttpKernel::class) + ->public() + ->args([ + service('event_dispatcher'), + service('controller_resolver'), + service('request_stack'), + service('argument_resolver'), + ]) + ->tag('container.hot_path') + ->tag('container.preload', ['class' => HttpKernelRunner::class]) + ->tag('container.preload', ['class' => ResponseRunner::class]) + ->tag('container.preload', ['class' => SymfonyRuntime::class]) + ->alias(HttpKernelInterface::class, 'http_kernel') + + ->set('request_stack', RequestStack::class) + ->public() + ->alias(RequestStack::class, 'request_stack') + + ->set('http_cache', HttpCache::class) + ->args([ + service('kernel'), + service('http_cache.store'), + service('esi')->nullOnInvalid(), + abstract_arg('options'), + ]) + ->tag('container.hot_path') + + ->set('http_cache.store', Store::class) + ->args([ + param('kernel.cache_dir').'/http_cache', + ]) + ->alias(StoreInterface::class, 'http_cache.store') + + ->set('url_helper', UrlHelper::class) + ->args([ + service('request_stack'), + service('router.request_context')->ignoreOnInvalid(), + ]) + ->alias(UrlHelper::class, 'url_helper') + + ->set('cache_warmer', CacheWarmerAggregate::class) + ->public() + ->args([ + tagged_iterator('kernel.cache_warmer'), + param('kernel.debug'), + sprintf('%s/%sDeprecations.log', param('kernel.build_dir'), param('kernel.container_class')), + ]) + ->tag('container.no_preload') + + ->set('cache_clearer', ChainCacheClearer::class) + ->args([ + tagged_iterator('kernel.cache_clearer'), + ]) + + ->set('kernel') + ->synthetic() + ->public() + ->alias(KernelInterface::class, 'kernel') + + ->set('filesystem', Filesystem::class) + ->alias(Filesystem::class, 'filesystem') + + ->set('file_locator', FileLocator::class) + ->args([ + service('kernel'), + ]) + ->alias(FileLocator::class, 'file_locator') + + ->set('uri_signer', UriSigner::class) + ->args([ + param('kernel.secret'), + ]) + ->alias(UriSigner::class, 'uri_signer') + + ->set('config_cache_factory', ResourceCheckerConfigCacheFactory::class) + ->args([ + tagged_iterator('config_cache.resource_checker'), + ]) + + ->set('dependency_injection.config.container_parameters_resource_checker', ContainerParametersResourceChecker::class) + ->args([ + service('service_container'), + ]) + ->tag('config_cache.resource_checker', ['priority' => -980]) + + ->set('config.resource.self_checking_resource_checker', SelfCheckingResourceChecker::class) + ->tag('config_cache.resource_checker', ['priority' => -990]) + + ->set('services_resetter', ServicesResetter::class) + ->public() + + ->set('reverse_container', ReverseContainer::class) + ->args([ + service('service_container'), + service_locator([]), + ]) + ->alias(ReverseContainer::class, 'reverse_container') + + ->set('locale_aware_listener', LocaleAwareListener::class) + ->args([ + [], // locale aware services + service('request_stack'), + ]) + ->tag('kernel.event_subscriber') + + ->set('container.env_var_processor', EnvVarProcessor::class) + ->args([ + service('service_container'), + tagged_iterator('container.env_var_loader'), + ]) + ->tag('container.env_var_processor') + + ->set('slugger', AsciiSlugger::class) + ->args([ + param('kernel.default_locale'), + ]) + ->tag('kernel.locale_aware') + ->alias(SluggerInterface::class, 'slugger') + + ->set('container.getenv', \Closure::class) + ->factory([\Closure::class, 'fromCallable']) + ->args([ + [service('service_container'), 'getEnv'], + ]) + ->tag('routing.expression_language_function', ['function' => 'env']) + + // inherit from this service to lazily access env vars + ->set('container.env', LazyString::class) + ->abstract() + ->factory([LazyString::class, 'fromCallable']) + ->args([ + service('container.getenv'), + ]) + ->set('config_builder.warmer', ConfigBuilderCacheWarmer::class) + ->args([service(KernelInterface::class), service('logger')->nullOnInvalid()]) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml deleted file mode 100644 index 5d8508f97dbe9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - console.command - console.error - console.terminate - form.pre_submit - form.submit - form.post_submit - form.pre_set_data - form.post_set_data - kernel.controller_arguments - kernel.controller - kernel.response - kernel.finish_request - kernel.request - kernel.view - kernel.exception - kernel.terminate - workflow.guard - workflow.leave - workflow.transition - workflow.enter - workflow.entered - workflow.completed - workflow.announce - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.debug% - %kernel.cache_dir%/%kernel.container_class%Deprecations.log - - - - - - - - - - - - - - - %kernel.root_dir%/Resources - - %kernel.root_dir% - - false - - - - - %kernel.secret% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php new file mode 100644 index 0000000000000..185e85838271c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpFoundation\Session\SessionFactory; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\IdentityMarshaller; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\MarshallingSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeFileSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\SessionHandlerFactory; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; +use Symfony\Component\HttpFoundation\Session\Storage\MockFileSessionStorageFactory; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorageFactory; +use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorageFactory; +use Symfony\Component\HttpKernel\EventListener\SessionListener; + +return static function (ContainerConfigurator $container) { + $container->parameters()->set('session.metadata.storage_key', '_sf2_meta'); + + $container->services() + ->set('session.factory', SessionFactory::class) + ->args([ + service('request_stack'), + service('session.storage.factory'), + [service('session_listener'), 'onSessionUsage'], + ]) + + ->set('session.storage.factory.native', NativeSessionStorageFactory::class) + ->args([ + param('session.storage.options'), + service('session.handler'), + inline_service(MetadataBag::class) + ->args([ + param('session.metadata.storage_key'), + param('session.metadata.update_threshold'), + ]), + false, + ]) + ->set('session.storage.factory.php_bridge', PhpBridgeSessionStorageFactory::class) + ->args([ + service('session.handler'), + inline_service(MetadataBag::class) + ->args([ + param('session.metadata.storage_key'), + param('session.metadata.update_threshold'), + ]), + false, + ]) + ->set('session.storage.factory.mock_file', MockFileSessionStorageFactory::class) + ->args([ + param('kernel.cache_dir').'/sessions', + 'MOCKSESSID', + inline_service(MetadataBag::class) + ->args([ + param('session.metadata.storage_key'), + param('session.metadata.update_threshold'), + ]), + ]) + + ->alias(\SessionHandlerInterface::class, 'session.handler') + + ->set('session.handler.native_file', StrictSessionHandler::class) + ->args([ + inline_service(NativeFileSessionHandler::class) + ->args([param('session.save_path')]), + ]) + + ->set('session.abstract_handler', AbstractSessionHandler::class) + ->factory([SessionHandlerFactory::class, 'createHandler']) + ->args([abstract_arg('A string or a connection object')]) + + ->set('session_listener', SessionListener::class) + ->args([ + service_locator([ + 'session_factory' => service('session.factory')->ignoreOnInvalid(), + 'logger' => service('logger')->ignoreOnInvalid(), + 'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(), + ]), + param('kernel.debug'), + param('session.storage.options'), + ]) + ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => 'reset']) + + ->set('session.marshaller', IdentityMarshaller::class) + + ->set('session.marshalling_handler', MarshallingSessionHandler::class) + ->decorate('session.handler') + ->args([ + service('session.marshalling_handler.inner'), + service('session.marshaller'), + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml deleted file mode 100644 index c9b17f311f576..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - _sf2_meta - - - - - - - - - - - - - - - %session.metadata.storage_key% - %session.metadata.update_threshold% - - - - %session.storage.options% - - - - - - - - - - - - - - - - - attributes - - - - %kernel.cache_dir%/sessions - MOCKSESSID - - - - - - - %session.save_path% - - - - - - - - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.1. Use the "session_listener" service instead. - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.php new file mode 100644 index 0000000000000..d41aa74d1e8dd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\HttpKernel\EventListener\SurrogateListener; +use Symfony\Component\HttpKernel\HttpCache\Ssi; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('ssi', Ssi::class) + + ->set('ssi_listener', SurrogateListener::class) + ->args([service('ssi')->ignoreOnInvalid()]) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml deleted file mode 100644 index b4e5b3d3df899..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/ssi.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml deleted file mode 100644 index b6b70be999de8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - %kernel.cache_dir% - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - %kernel.root_dir%/Resources - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - %templating.loader.cache.path% - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml deleted file mode 100644 index 3dd7b84c123ba..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - %kernel.charset% - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml deleted file mode 100644 index 440b9a5d23298..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/templating_php.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - %kernel.charset% - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - %kernel.project_dir% - %kernel.charset% - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - %templating.helper.form.resources% - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php new file mode 100644 index 0000000000000..cef5dfc4ce80d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\TestContainer; +use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\BrowserKit\History; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpKernel\EventListener\SessionListener; + +return static function (ContainerConfigurator $container) { + $container->parameters()->set('test.client.parameters', []); + + $container->services() + ->set('test.client', KernelBrowser::class) + ->args([ + service('kernel'), + param('test.client.parameters'), + service('test.client.history'), + service('test.client.cookiejar'), + ]) + ->share(false) + ->public() + + ->set('test.client.history', History::class)->share(false) + ->set('test.client.cookiejar', CookieJar::class)->share(false) + + ->set('test.session.listener', SessionListener::class) + ->args([ + service_locator([ + 'session_factory' => service('session.factory')->ignoreOnInvalid(), + ]), + param('kernel.debug'), + param('session.storage.options'), + ]) + ->tag('kernel.event_subscriber') + + ->set('test.service_container', TestContainer::class) + ->args([ + service('kernel'), + 'test.private_services_locator', + ]) + ->public() + + ->set('test.private_services_locator', ServiceLocator::class) + ->args([abstract_arg('callable collection')]) + ->public() + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml deleted file mode 100644 index ef571fdbc6748..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - %test.client.parameters% - - - - - - - - - - - - - - - - - - test.private_services_locator - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php new file mode 100644 index 0000000000000..706e4928ee2e0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\TranslationsCacheWarmer; +use Symfony\Bundle\FrameworkBundle\Translation\Translator; +use Symfony\Component\Translation\Dumper\CsvFileDumper; +use Symfony\Component\Translation\Dumper\IcuResFileDumper; +use Symfony\Component\Translation\Dumper\IniFileDumper; +use Symfony\Component\Translation\Dumper\JsonFileDumper; +use Symfony\Component\Translation\Dumper\MoFileDumper; +use Symfony\Component\Translation\Dumper\PhpFileDumper; +use Symfony\Component\Translation\Dumper\PoFileDumper; +use Symfony\Component\Translation\Dumper\QtFileDumper; +use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Dumper\YamlFileDumper; +use Symfony\Component\Translation\Extractor\ChainExtractor; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\Extractor\PhpExtractor; +use Symfony\Component\Translation\Formatter\MessageFormatter; +use Symfony\Component\Translation\Loader\CsvFileLoader; +use Symfony\Component\Translation\Loader\IcuDatFileLoader; +use Symfony\Component\Translation\Loader\IcuResFileLoader; +use Symfony\Component\Translation\Loader\IniFileLoader; +use Symfony\Component\Translation\Loader\JsonFileLoader; +use Symfony\Component\Translation\Loader\MoFileLoader; +use Symfony\Component\Translation\Loader\PhpFileLoader; +use Symfony\Component\Translation\Loader\PoFileLoader; +use Symfony\Component\Translation\Loader\QtFileLoader; +use Symfony\Component\Translation\Loader\XliffFileLoader; +use Symfony\Component\Translation\Loader\YamlFileLoader; +use Symfony\Component\Translation\LoggingTranslator; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriter; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translator.default', Translator::class) + ->args([ + abstract_arg('translation loaders locator'), + service('translator.formatter'), + param('kernel.default_locale'), + abstract_arg('translation loaders ids'), + [ + 'cache_dir' => param('kernel.cache_dir').'/translations', + 'debug' => param('kernel.debug'), + ], + abstract_arg('enabled locales'), + ]) + ->call('setConfigCacheFactory', [service('config_cache_factory')]) + ->tag('kernel.locale_aware') + + ->alias(TranslatorInterface::class, 'translator') + + ->set('translator.logging', LoggingTranslator::class) + ->args([ + service('translator.logging.inner'), + service('logger'), + ]) + ->tag('monolog.logger', ['channel' => 'translation']) + + ->set('translator.formatter.default', MessageFormatter::class) + ->args([service('identity_translator')]) + + ->set('translation.loader.php', PhpFileLoader::class) + ->tag('translation.loader', ['alias' => 'php']) + + ->set('translation.loader.yml', YamlFileLoader::class) + ->tag('translation.loader', ['alias' => 'yaml', 'legacy-alias' => 'yml']) + + ->set('translation.loader.xliff', XliffFileLoader::class) + ->tag('translation.loader', ['alias' => 'xlf', 'legacy-alias' => 'xliff']) + + ->set('translation.loader.po', PoFileLoader::class) + ->tag('translation.loader', ['alias' => 'po']) + + ->set('translation.loader.mo', MoFileLoader::class) + ->tag('translation.loader', ['alias' => 'mo']) + + ->set('translation.loader.qt', QtFileLoader::class) + ->tag('translation.loader', ['alias' => 'ts']) + + ->set('translation.loader.csv', CsvFileLoader::class) + ->tag('translation.loader', ['alias' => 'csv']) + + ->set('translation.loader.res', IcuResFileLoader::class) + ->tag('translation.loader', ['alias' => 'res']) + + ->set('translation.loader.dat', IcuDatFileLoader::class) + ->tag('translation.loader', ['alias' => 'dat']) + + ->set('translation.loader.ini', IniFileLoader::class) + ->tag('translation.loader', ['alias' => 'ini']) + + ->set('translation.loader.json', JsonFileLoader::class) + ->tag('translation.loader', ['alias' => 'json']) + + ->set('translation.dumper.php', PhpFileDumper::class) + ->tag('translation.dumper', ['alias' => 'php']) + + ->set('translation.dumper.xliff', XliffFileDumper::class) + ->tag('translation.dumper', ['alias' => 'xlf']) + + ->set('translation.dumper.po', PoFileDumper::class) + ->tag('translation.dumper', ['alias' => 'po']) + + ->set('translation.dumper.mo', MoFileDumper::class) + ->tag('translation.dumper', ['alias' => 'mo']) + + ->set('translation.dumper.yml', YamlFileDumper::class) + ->tag('translation.dumper', ['alias' => 'yml']) + + ->set('translation.dumper.yaml', YamlFileDumper::class) + ->args(['yaml']) + ->tag('translation.dumper', ['alias' => 'yaml']) + + ->set('translation.dumper.qt', QtFileDumper::class) + ->tag('translation.dumper', ['alias' => 'ts']) + + ->set('translation.dumper.csv', CsvFileDumper::class) + ->tag('translation.dumper', ['alias' => 'csv']) + + ->set('translation.dumper.ini', IniFileDumper::class) + ->tag('translation.dumper', ['alias' => 'ini']) + + ->set('translation.dumper.json', JsonFileDumper::class) + ->tag('translation.dumper', ['alias' => 'json']) + + ->set('translation.dumper.res', IcuResFileDumper::class) + ->tag('translation.dumper', ['alias' => 'res']) + + ->set('translation.extractor.php', PhpExtractor::class) + ->tag('translation.extractor', ['alias' => 'php']) + + ->set('translation.reader', TranslationReader::class) + ->alias(TranslationReaderInterface::class, 'translation.reader') + + ->set('translation.extractor', ChainExtractor::class) + ->alias(ExtractorInterface::class, 'translation.extractor') + + ->set('translation.writer', TranslationWriter::class) + ->alias(TranslationWriterInterface::class, 'translation.writer') + + ->set('translation.warmer', TranslationsCacheWarmer::class) + ->args([service(ContainerInterface::class)]) + ->tag('container.service_subscriber', ['id' => 'translator']) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml deleted file mode 100644 index a9d5a85d202e8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - %kernel.default_locale% - - - %kernel.cache_dir%/translations - %kernel.debug% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - yaml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.php new file mode 100644 index 0000000000000..7a83301811f28 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Translation\DataCollector\TranslationDataCollector; +use Symfony\Component\Translation\DataCollectorTranslator; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translator.data_collector', DataCollectorTranslator::class) + ->args([service('translator.data_collector.inner')]) + + ->set('data_collector.translation', TranslationDataCollector::class) + ->args([service('translator.data_collector')]) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/translation.html.twig', + 'id' => 'translation', + 'priority' => 275, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml deleted file mode 100644 index c9c5385fbfb76..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_providers.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_providers.php new file mode 100644 index 0000000000000..cd140f077c172 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation_providers.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory; +use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory; +use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory; +use Symfony\Component\Translation\Provider\NullProviderFactory; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Provider\TranslationProviderCollectionFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('translation.provider_collection', TranslationProviderCollection::class) + ->factory([service('translation.provider_collection_factory'), 'fromConfig']) + ->args([ + [], // Providers + ]) + + ->set('translation.provider_collection_factory', TranslationProviderCollectionFactory::class) + ->args([ + tagged_iterator('translation.provider_factory'), + [], // Enabled locales + ]) + + ->set('translation.provider_factory.null', NullProviderFactory::class) + ->tag('translation.provider_factory') + + ->set('translation.provider_factory.crowdin', CrowdinProviderFactory::class) + ->args([ + service('http_client'), + service('logger'), + param('kernel.default_locale'), + service('translation.loader.xliff'), + service('translation.dumper.xliff'), + ]) + ->tag('translation.provider_factory') + + ->set('translation.provider_factory.loco', LocoProviderFactory::class) + ->args([ + service('http_client'), + service('logger'), + param('kernel.default_locale'), + service('translation.loader.xliff'), + ]) + ->tag('translation.provider_factory') + + ->set('translation.provider_factory.lokalise', LokaliseProviderFactory::class) + ->args([ + service('http_client'), + service('logger'), + param('kernel.default_locale'), + service('translation.loader.xliff'), + ]) + ->tag('translation.provider_factory') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php new file mode 100644 index 0000000000000..840fb97b5f5f5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/uid.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Uid\Factory\NameBasedUuidFactory; +use Symfony\Component\Uid\Factory\RandomBasedUuidFactory; +use Symfony\Component\Uid\Factory\TimeBasedUuidFactory; +use Symfony\Component\Uid\Factory\UlidFactory; +use Symfony\Component\Uid\Factory\UuidFactory; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('ulid.factory', UlidFactory::class) + ->alias(UlidFactory::class, 'ulid.factory') + + ->set('uuid.factory', UuidFactory::class) + ->alias(UuidFactory::class, 'uuid.factory') + + ->set('name_based_uuid.factory', NameBasedUuidFactory::class) + ->factory([service('uuid.factory'), 'nameBased']) + ->args([abstract_arg('Please set the "framework.uid.name_based_uuid_namespace" configuration option to use the "name_based_uuid.factory" service')]) + ->alias(NameBasedUuidFactory::class, 'name_based_uuid.factory') + + ->set('random_based_uuid.factory', RandomBasedUuidFactory::class) + ->factory([service('uuid.factory'), 'randomBased']) + ->alias(RandomBasedUuidFactory::class, 'random_based_uuid.factory') + + ->set('time_based_uuid.factory', TimeBasedUuidFactory::class) + ->factory([service('uuid.factory'), 'timeBased']) + ->alias(TimeBasedUuidFactory::class, 'time_based_uuid.factory') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php new file mode 100644 index 0000000000000..2eaa5b5ec2c78 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Validator\Constraints\EmailValidator; +use Symfony\Component\Validator\Constraints\ExpressionValidator; +use Symfony\Component\Validator\Constraints\NotCompromisedPasswordValidator; +use Symfony\Component\Validator\ContainerConstraintValidatorFactory; +use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Validator\ValidatorBuilder; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('validator.mapping.cache.file', param('kernel.cache_dir').'/validation.php'); + + $container->services() + ->set('validator', ValidatorInterface::class) + ->factory([service('validator.builder'), 'getValidator']) + ->alias(ValidatorInterface::class, 'validator') + + ->set('validator.builder', ValidatorBuilder::class) + ->factory([Validation::class, 'createValidatorBuilder']) + ->call('setConstraintValidatorFactory', [ + service('validator.validator_factory'), + ]) + ->call('setTranslator', [ + service('translator')->ignoreOnInvalid(), + ]) + ->call('setTranslationDomain', [ + param('validator.translation_domain'), + ]) + ->alias('validator.mapping.class_metadata_factory', 'validator') + + ->set('validator.mapping.cache_warmer', ValidatorCacheWarmer::class) + ->args([ + service('validator.builder'), + param('validator.mapping.cache.file'), + ]) + ->tag('kernel.cache_warmer') + + ->set('validator.mapping.cache.adapter', PhpArrayAdapter::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([ + param('validator.mapping.cache.file'), + service('cache.validator'), + ]) + + ->set('validator.validator_factory', ContainerConstraintValidatorFactory::class) + ->args([ + abstract_arg('Constraint validators locator'), + ]) + + ->set('validator.expression', ExpressionValidator::class) + ->args([service('validator.expression_language')->nullOnInvalid()]) + ->tag('validator.constraint_validator', [ + 'alias' => 'validator.expression', + ]) + + ->set('validator.expression_language', ExpressionLanguage::class) + ->args([service('cache.validator_expression_language')->nullOnInvalid()]) + + ->set('cache.validator_expression_language') + ->parent('cache.system') + ->tag('cache.pool') + + ->set('validator.email', EmailValidator::class) + ->args([ + abstract_arg('Default mode'), + ]) + ->tag('validator.constraint_validator', [ + 'alias' => EmailValidator::class, + ]) + + ->set('validator.not_compromised_password', NotCompromisedPasswordValidator::class) + ->args([ + service('http_client')->nullOnInvalid(), + param('kernel.charset'), + false, + ]) + ->tag('validator.constraint_validator', [ + 'alias' => NotCompromisedPasswordValidator::class, + ]) + + ->set('validator.property_info_loader', PropertyInfoLoader::class) + ->args([ + service('property_info'), + service('property_info'), + service('property_info'), + ]) + ->tag('validator.auto_mapper') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml deleted file mode 100644 index 886132ff4b1c8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - %kernel.cache_dir%/validation.php - - - - - - - - - - - - - - - - - - - - %validator.translation_domain% - - - - - - - - %validator.mapping.cache.file% - - - - - - The "%service_id%" service is deprecated since Symfony 4.4. Use validator.mapping.cache.adapter instead. - - - - - %validator.mapping.cache.file% - - - - - - - - - - - - - - - - - - - %kernel.charset% - false - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.php new file mode 100644 index 0000000000000..e9fe441140742 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Validator\DataCollector\ValidatorDataCollector; +use Symfony\Component\Validator\Validator\TraceableValidator; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('debug.validator', TraceableValidator::class) + ->decorate('validator', null, 255) + ->args([ + service('debug.validator.inner'), + ]) + ->tag('kernel.reset', [ + 'method' => 'reset', + ]) + + ->set('data_collector.validator', ValidatorDataCollector::class) + ->args([ + service('debug.validator'), + ]) + ->tag('data_collector', [ + 'template' => '@WebProfiler/Collector/validator.html.twig', + 'id' => 'validator', + 'priority' => 320, + ]) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml deleted file mode 100644 index 939c55553ca38..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator_debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php new file mode 100644 index 0000000000000..53613d3b5020c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; +use Symfony\Component\HttpKernel\Controller\ErrorController; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener; +use Symfony\Component\HttpKernel\EventListener\ErrorListener; +use Symfony\Component\HttpKernel\EventListener\LocaleListener; +use Symfony\Component\HttpKernel\EventListener\ResponseListener; +use Symfony\Component\HttpKernel\EventListener\StreamedResponseListener; +use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('controller_resolver', ControllerResolver::class) + ->args([ + service('service_container'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'request']) + + ->set('argument_metadata_factory', ArgumentMetadataFactory::class) + + ->set('argument_resolver', ArgumentResolver::class) + ->args([ + service('argument_metadata_factory'), + abstract_arg('argument value resolvers'), + ]) + + ->set('argument_resolver.request_attribute', RequestAttributeValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 100]) + + ->set('argument_resolver.request', RequestValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50]) + + ->set('argument_resolver.session', SessionValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => 50]) + + ->set('argument_resolver.service', ServiceValueResolver::class) + ->args([ + abstract_arg('service locator, set in RegisterControllerArgumentLocatorsPass'), + ]) + ->tag('controller.argument_value_resolver', ['priority' => -50]) + + ->set('argument_resolver.default', DefaultValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => -100]) + + ->set('argument_resolver.variadic', VariadicValueResolver::class) + ->tag('controller.argument_value_resolver', ['priority' => -150]) + + ->set('response_listener', ResponseListener::class) + ->args([ + param('kernel.charset'), + abstract_arg('The "set_content_language_from_locale" config value'), + ]) + ->tag('kernel.event_subscriber') + + ->set('streamed_response_listener', StreamedResponseListener::class) + ->tag('kernel.event_subscriber') + + ->set('locale_listener', LocaleListener::class) + ->args([ + service('request_stack'), + param('kernel.default_locale'), + service('router')->ignoreOnInvalid(), + abstract_arg('The "set_locale_from_accept_language" config value'), + param('kernel.enabled_locales'), + ]) + ->tag('kernel.event_subscriber') + + ->set('validate_request_listener', ValidateRequestListener::class) + ->tag('kernel.event_subscriber') + + ->set('disallow_search_engine_index_response_listener', DisallowRobotsIndexingListener::class) + ->tag('kernel.event_subscriber') + + ->set('error_controller', ErrorController::class) + ->public() + ->args([ + service('http_kernel'), + param('kernel.error_controller'), + service('error_renderer'), + ]) + + ->set('exception_listener', ErrorListener::class) + ->args([ + param('kernel.error_controller'), + service('logger')->nullOnInvalid(), + param('kernel.debug'), + abstract_arg('an exceptions to log & status code mapping'), + ]) + ->tag('kernel.event_subscriber') + ->tag('monolog.logger', ['channel' => 'request']) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml deleted file mode 100644 index aff90a584b87d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - false - - - - The "%alias_id%" service is deprecated since Symfony 4.3. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %kernel.charset% - - - - - - - - - - %kernel.default_locale% - - - - - - - - - - false - - - - The "%alias_id%" service is deprecated since Symfony 4.3. - - - - - - - - - %kernel.error_controller% - - - - - - - %kernel.error_controller% - - %kernel.debug% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.php new file mode 100644 index 0000000000000..0b0e79db8c1bf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('web_link.add_link_header_listener', AddLinkHeaderListener::class) + ->tag('kernel.event_subscriber') + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.xml deleted file mode 100644 index bf3e8d7211e00..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web_link.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.php new file mode 100644 index 0000000000000..b6c784bdbeaa9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Workflow\EventListener\ExpressionLanguage; +use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore; +use Symfony\Component\Workflow\Registry; +use Symfony\Component\Workflow\StateMachine; +use Symfony\Component\Workflow\Workflow; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('workflow.abstract', Workflow::class) + ->args([ + abstract_arg('workflow definition'), + abstract_arg('marking store'), + service('event_dispatcher')->ignoreOnInvalid(), + abstract_arg('workflow name'), + abstract_arg('events to dispatch'), + ]) + ->abstract() + ->set('state_machine.abstract', StateMachine::class) + ->args([ + abstract_arg('workflow definition'), + abstract_arg('marking store'), + service('event_dispatcher')->ignoreOnInvalid(), + abstract_arg('workflow name'), + abstract_arg('events to dispatch'), + ]) + ->abstract() + ->set('workflow.marking_store.method', MethodMarkingStore::class) + ->abstract() + ->set('workflow.registry', Registry::class) + ->alias(Registry::class, 'workflow.registry') + ->set('workflow.security.expression_language', ExpressionLanguage::class) + ; +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml deleted file mode 100644 index 0bba153b10317..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - null - - - - - - null - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php deleted file mode 100644 index 047d342a8d5c3..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/attributes.html.php +++ /dev/null @@ -1,9 +0,0 @@ - $v): ?> - -escape($k), $view->escape(false !== $translation_domain ? $view['translator']->trans($v, $attr_translation_parameters, $translation_domain) : $v)) ?> - -escape($k), $view->escape($k)) ?> - -escape($k), $view->escape($v)) ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php deleted file mode 100644 index 279233baa3fc0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_attributes.html.php +++ /dev/null @@ -1,2 +0,0 @@ -id="escape($id) ?>" name="escape($full_name) ?>" disabled="disabled" -block($form, 'attributes') : '' ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_row.html.php deleted file mode 100644 index b52e92984533d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_row.html.php +++ /dev/null @@ -1,3 +0,0 @@ -
- widget($form) ?> -
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php deleted file mode 100644 index 42a7c5db02d8f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/button_widget.html.php +++ /dev/null @@ -1,4 +0,0 @@ - $name, '%id%' => $id]) - : $view['form']->humanize($name); } ?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php deleted file mode 100644 index 143557dea8a64..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/checkbox_widget.html.php +++ /dev/null @@ -1,5 +0,0 @@ -block($form, 'widget_attributes') ?> - 0): ?> value="escape($value) ?>" - checked="checked" -/> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php deleted file mode 100644 index 18f8368dc8065..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_attributes.html.php +++ /dev/null @@ -1,8 +0,0 @@ -disabled="disabled" - $v): ?> - -escape($k), $view->escape($k)) ?> - -escape($k), $view->escape($v)) ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php deleted file mode 100644 index 211ae73f1c3d1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_options.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'choice_widget_options') ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php deleted file mode 100644 index 13593a96f11ef..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget.html.php +++ /dev/null @@ -1,5 +0,0 @@ - -block($form, 'choice_widget_expanded') ?> - -block($form, 'choice_widget_collapsed') ?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php deleted file mode 100644 index 6a57d585c7b57..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_collapsed.html.php +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_expanded.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_expanded.html.php deleted file mode 100644 index 9691759a907f5..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_expanded.html.php +++ /dev/null @@ -1,6 +0,0 @@ -
block($form, 'widget_container_attributes') ?>> - - widget($child) ?> - label($child, null, ['translation_domain' => $choice_translation_domain]) ?> - -
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php deleted file mode 100644 index a19b5a5e6e0b8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/choice_widget_options.html.php +++ /dev/null @@ -1,13 +0,0 @@ - - - $choice): ?> - - - block($form, 'choice_widget_options', ['choices' => $choice]) ?> - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/collection_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/collection_widget.html.php deleted file mode 100644 index 40dfdaf1f031f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/collection_widget.html.php +++ /dev/null @@ -1,4 +0,0 @@ - - escape($view['form']->row($prototype)) ?> - -widget($form, ['attr' => $attr]) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php deleted file mode 100644 index 74b5e6f72c23e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'color']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php deleted file mode 100644 index 302bbfcd479d9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/container_attributes.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'widget_container_attributes') ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php deleted file mode 100644 index fbd844f28693b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/date_widget.html.php +++ /dev/null @@ -1,11 +0,0 @@ - - block($form, 'form_widget_simple'); ?> - -
block($form, 'widget_container_attributes') ?>> - widget($form['year']), - $view['form']->widget($form['month']), - $view['form']->widget($form['day']), - ], $date_pattern) ?> -
- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php deleted file mode 100644 index aef2a32512f72..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/datetime_widget.html.php +++ /dev/null @@ -1,7 +0,0 @@ - - block($form, 'form_widget_simple'); ?> - -
block($form, 'widget_container_attributes') ?>> - widget($form['date']).' '.$view['form']->widget($form['time']) ?> -
- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php deleted file mode 100644 index 96bfbcacc2006..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'email']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php deleted file mode 100644 index fb789faff723a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php +++ /dev/null @@ -1,3 +0,0 @@ -start($form) ?> - widget($form) ?> -end($form) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_enctype.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_enctype.html.php deleted file mode 100644 index 36eba3c9e8e7a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_enctype.html.php +++ /dev/null @@ -1 +0,0 @@ -vars['multipart']): ?>enctype="multipart/form-data" diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php deleted file mode 100644 index fe6843905cee5..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_end.html.php +++ /dev/null @@ -1,4 +0,0 @@ - -rest($form) ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_errors.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_errors.html.php deleted file mode 100644 index d97179e9a680c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_errors.html.php +++ /dev/null @@ -1,7 +0,0 @@ - 0): ?> -
    - -
  • escape($error->getMessage()) ?>
  • - -
- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_help.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_help.html.php deleted file mode 100644 index 9e3ed9b6c6e44..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_help.html.php +++ /dev/null @@ -1,6 +0,0 @@ - - - trans($help, $help_translation_parameters, $translation_domain) : $help; ?> - escape($help) : $help ?> -

block($form, 'attributes', ['attr' => $help_attr]); ?>>

- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php deleted file mode 100644 index a17a5ca2ab26f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php +++ /dev/null @@ -1,8 +0,0 @@ - - - - $name, '%id%' => $id]) - : $view['form']->humanize($name); } ?> -block($form, 'attributes', ['attr' => $label_attr]); } ?>>escape(false !== $translation_domain ? $view['translator']->trans($label, $label_translation_parameters, $translation_domain) : $label) ?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_rest.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_rest.html.php deleted file mode 100644 index 89041c6ec6374..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_rest.html.php +++ /dev/null @@ -1,5 +0,0 @@ - - isRendered()): ?> - row($child) ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php deleted file mode 100644 index b6e750aa79f4b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_row.html.php +++ /dev/null @@ -1,7 +0,0 @@ -
- ['aria-describedby' => $id.'_help']]; ?> - label($form); ?> - errors($form); ?> - widget($form, $widgetAttr); ?> - help($form); ?> -
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_rows.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_rows.html.php deleted file mode 100644 index 4de226f2b8b2a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_rows.html.php +++ /dev/null @@ -1,5 +0,0 @@ - - isRendered()): ?> - row($child) ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php deleted file mode 100644 index 7e244258053ff..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_start.html.php +++ /dev/null @@ -1,6 +0,0 @@ - - -
action="escape($action) ?>" $v) { printf(' %s="%s"', $view->escape($k), $view->escape($v)); } ?> enctype="multipart/form-data"> - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php deleted file mode 100644 index c5af39a5b6b06..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget.html.php +++ /dev/null @@ -1,5 +0,0 @@ - -block($form, 'form_widget_compound')?> - -block($form, 'form_widget_simple')?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_compound.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_compound.html.php deleted file mode 100644 index 7a4f7cd51fb3a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_compound.html.php +++ /dev/null @@ -1,7 +0,0 @@ -
block($form, 'widget_container_attributes') ?>> - parent && $errors): ?> - errors($form) ?> - - block($form, 'form_rows') ?> - rest($form) ?> -
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php deleted file mode 100644 index 5d7654f54cdc6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_widget_simple.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'widget_attributes') ?> value="escape($value) ?>" /> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_row.html.php deleted file mode 100644 index 3239d8f415b12..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_row.html.php +++ /dev/null @@ -1 +0,0 @@ -widget($form) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php deleted file mode 100644 index 15df70208e530..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'hidden']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php deleted file mode 100644 index 64ae6b76ec10d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'number']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php deleted file mode 100644 index 25fe13f7e057c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/money_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -formEncodeCurrency($money_pattern, $view['form']->block($form, 'form_widget_simple')) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php deleted file mode 100644 index 225aa31573664..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'text']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php deleted file mode 100644 index ea6c6ab479c60..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'password']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php deleted file mode 100644 index 47ae89f50a261..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php +++ /dev/null @@ -1,2 +0,0 @@ - -block($form, 'form_widget_simple', ['type' => $type ?? 'text']).$view->escape($symbol) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php deleted file mode 100644 index ddc8c529dff67..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/radio_widget.html.php +++ /dev/null @@ -1,5 +0,0 @@ -block($form, 'widget_attributes') ?> - value="escape($value) ?>" - checked="checked" -/> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php deleted file mode 100644 index 97f4a497f5d68..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'range']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/repeated_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/repeated_row.html.php deleted file mode 100644 index d4af23d712320..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/repeated_row.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_rows') ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php deleted file mode 100644 index 117327b16ea98..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'button_widget', ['type' => $type ?? 'reset']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php deleted file mode 100644 index 6b6efa7b75ed5..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'search']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php deleted file mode 100644 index db643c24235e8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'button_widget', ['type' => $type ?? 'submit']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php deleted file mode 100644 index fb62226c1d7e2..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'tel']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php deleted file mode 100644 index c989ce575d579..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/textarea_widget.html.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php deleted file mode 100644 index cd2f5596011c1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/time_widget.html.php +++ /dev/null @@ -1,22 +0,0 @@ - - block($form, 'form_widget_simple'); ?> - - ['size' => 1]] : [] ?> -
block($form, 'widget_container_attributes') ?>> - widget($form['hour'], $vars); - - if ($with_minutes) { - echo ':'; - echo $view['form']->widget($form['minute'], $vars); - } - - if ($with_seconds) { - echo ':'; - echo $view['form']->widget($form['second'], $vars); - } - ?> -
- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php deleted file mode 100644 index fee20f07c979c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php +++ /dev/null @@ -1 +0,0 @@ -block($form, 'form_widget_simple', ['type' => $type ?? 'url']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/week_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/week_widget.html.php deleted file mode 100644 index 610b6e0c19eac..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/week_widget.html.php +++ /dev/null @@ -1,14 +0,0 @@ - - block($form, 'form_widget_simple'); ?> - - ['size' => 1]] : [] ?> -
block($form, 'widget_container_attributes') ?>> - widget($form['year'], $vars); - echo '-'; - echo $view['form']->widget($form['week'], $vars); - ?> -
- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php deleted file mode 100644 index 1626a9cc63ff5..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_attributes.html.php +++ /dev/null @@ -1,3 +0,0 @@ -id="escape($id) ?>" name="escape($full_name) ?>" disabled="disabled" - required="required" -block($form, 'attributes') : '' ?> \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php deleted file mode 100644 index fdd176d12c79f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/widget_container_attributes.html.php +++ /dev/null @@ -1,2 +0,0 @@ -id="escape($id) ?>" -block($form, 'attributes') : '' ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/button_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/button_row.html.php deleted file mode 100644 index 67d0137e20b97..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/button_row.html.php +++ /dev/null @@ -1,6 +0,0 @@ - - - - widget($form); ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php deleted file mode 100644 index 92b87e1b679bd..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_row.html.php +++ /dev/null @@ -1,11 +0,0 @@ - - ['aria-describedby' => $id.'_help']]; ?> - - label($form); ?> - - - errors($form); ?> - widget($form, $widgetAttr); ?> - help($form); ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget_compound.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget_compound.html.php deleted file mode 100644 index adc897338861b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/form_widget_compound.html.php +++ /dev/null @@ -1,11 +0,0 @@ -block($form, 'widget_container_attributes'); ?>> - parent && $errors): ?> - - - - - block($form, 'form_rows'); ?> - rest($form); ?> -
- errors($form); ?> -
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/hidden_row.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/hidden_row.html.php deleted file mode 100644 index 116b300bd5619..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/FormTable/hidden_row.html.php +++ /dev/null @@ -1,5 +0,0 @@ - - - widget($form); ?> - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php index 6e5a71b8fadfd..7e6fa732090eb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php @@ -24,10 +24,8 @@ class AnnotatedRouteControllerLoader extends AnnotationClassLoader { /** * Configures the _controller default parameter of a given Route instance. - * - * @param object $annot The annotation class instance */ - protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) + protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot) { if ('__invoke' === $method->getName()) { $route->setDefault('_controller', $class->getName()); @@ -38,19 +36,15 @@ protected function configureRoute(Route $route, \ReflectionClass $class, \Reflec /** * Makes the default route name more sane by removing common keywords. - * - * @return string */ - protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method): string { - return preg_replace([ - '/(bundle|controller)_/', - '/action(_\d+)?$/', - '/__/', - ], [ - '_', - '\\1', - '_', - ], parent::getDefaultRouteName($class, $method)); + $name = preg_replace('/(bundle|controller)_/', '_', parent::getDefaultRouteName($class, $method)); + + if (str_ends_with($method->name, 'Action') || str_ends_with($method->name, '_action')) { + $name = preg_replace('/action(_\d+)?$/', '\\1', $name); + } + + return str_replace('__', '_', $name); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php index 7276a387d4e01..a0fefe7fd2fab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php @@ -11,10 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Routing; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Component\Config\Exception\LoaderLoadException; use Symfony\Component\Config\Loader\DelegatingLoader as BaseDelegatingLoader; use Symfony\Component\Config\Loader\LoaderResolverInterface; +use Symfony\Component\Routing\RouteCollection; /** * DelegatingLoader delegates route loading to other loaders using a loader resolver. @@ -24,33 +24,18 @@ * * @author Fabien Potencier * - * @final since Symfony 4.4 + * @final */ class DelegatingLoader extends BaseDelegatingLoader { - /** - * @deprecated since Symfony 4.4 - */ - protected $parser; - private $loading = false; - private $defaultOptions; + private bool $loading = false; + private array $defaultOptions; + private array $defaultRequirements; - /** - * @param LoaderResolverInterface $resolver - * @param array $defaultOptions - */ - public function __construct($resolver, $defaultOptions = []) + public function __construct(LoaderResolverInterface $resolver, array $defaultOptions = [], array $defaultRequirements = []) { - if ($resolver instanceof ControllerNameParser) { - @trigger_error(sprintf('Passing a "%s" instance as first argument to "%s()" is deprecated since Symfony 4.4, pass a "%s" instance instead.', ControllerNameParser::class, __METHOD__, LoaderResolverInterface::class), \E_USER_DEPRECATED); - $this->parser = $resolver; - $resolver = $defaultOptions; - $defaultOptions = 2 < \func_num_args() ? func_get_arg(2) : []; - } elseif (2 < \func_num_args() && func_get_arg(2) instanceof ControllerNameParser) { - $this->parser = func_get_arg(2); - } - $this->defaultOptions = $defaultOptions; + $this->defaultRequirements = $defaultRequirements; parent::__construct($resolver); } @@ -58,7 +43,7 @@ public function __construct($resolver, $defaultOptions = []) /** * {@inheritdoc} */ - public function load($resource, $type = null) + public function load(mixed $resource, string $type = null): RouteCollection { if ($this->loading) { // This can happen if a fatal error occurs in parent::load(). @@ -91,6 +76,9 @@ public function load($resource, $type = null) if ($this->defaultOptions) { $route->setOptions($route->getOptions() + $this->defaultOptions); } + if ($this->defaultRequirements) { + $route->setRequirements($route->getRequirements() + $this->defaultRequirements); + } if (!\is_string($controller = $route->getDefault('_controller'))) { continue; } @@ -99,18 +87,6 @@ public function load($resource, $type = null) continue; } - if ($this->parser && 2 === substr_count($controller, ':')) { - $deprecatedNotation = $controller; - - try { - $controller = $this->parser->parse($controller, false); - - @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $deprecatedNotation, $controller), \E_USER_DEPRECATED); - } catch (\InvalidArgumentException $e) { - // unable to optimize unknown notation - } - } - $route->setDefault('_controller', $controller); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/LegacyRouteLoaderContainer.php b/src/Symfony/Bundle/FrameworkBundle/Routing/LegacyRouteLoaderContainer.php deleted file mode 100644 index 10529b459a40d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/LegacyRouteLoaderContainer.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Routing; - -use Psr\Container\ContainerInterface; - -/** - * @internal to be removed in Symfony 5.0 - */ -class LegacyRouteLoaderContainer implements ContainerInterface -{ - private $container; - private $serviceLocator; - - public function __construct(ContainerInterface $container, ContainerInterface $serviceLocator) - { - $this->container = $container; - $this->serviceLocator = $serviceLocator; - } - - /** - * {@inheritdoc} - * - * @return mixed - */ - public function get($id) - { - if ($this->serviceLocator->has($id)) { - return $this->serviceLocator->get($id); - } - - @trigger_error(sprintf('Registering the service route loader "%s" without tagging it with the "routing.route_loader" tag is deprecated since Symfony 4.4 and will be required in Symfony 5.0.', $id), \E_USER_DEPRECATED); - - return $this->container->get($id); - } - - /** - * {@inheritdoc} - * - * @return bool - */ - public function has($id) - { - return $this->serviceLocator->has($id) || $this->container->has($id); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php index cb2c831d969fd..dba9d6d9613d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php @@ -24,7 +24,7 @@ class RedirectableCompiledUrlMatcher extends CompiledUrlMatcher implements Redir /** * {@inheritdoc} */ - public function redirect($path, $route, $scheme = null): array + public function redirect(string $path, string $route, string $scheme = null): array { return [ '_controller' => 'Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction', diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php deleted file mode 100644 index c9ca4831845c9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Routing; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3.', RedirectableUrlMatcher::class), \E_USER_DEPRECATED); - -use Symfony\Component\Routing\Matcher\RedirectableUrlMatcher as BaseMatcher; - -/** - * @author Fabien Potencier - * - * @deprecated since Symfony 4.3 - */ -class RedirectableUrlMatcher extends BaseMatcher -{ - /** - * Redirects the user to another URL. - * - * @param string $path The path info to redirect to - * @param string $route The route that matched - * @param string $scheme The URL scheme (null to keep the current one) - * - * @return array An array of parameters - */ - public function redirect($path, $route, $scheme = null) - { - return [ - '_controller' => 'Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction', - 'path' => $path, - 'permanent' => true, - 'scheme' => $scheme, - 'httpPort' => $this->context->getHttpPort(), - 'httpsPort' => $this->context->getHttpsPort(), - '_route' => $route, - ]; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index de23e474fc16c..cb442ca7ccdaf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -13,7 +13,6 @@ use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\CompatibilityServiceSubscriberInterface as ServiceSubscriberInterface; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Config\Resource\FileResource; @@ -22,14 +21,10 @@ use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; -use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Router as BaseRouter; - -// Help opcache.preload discover always-needed symbols -class_exists(RedirectableCompiledUrlMatcher::class); -class_exists(Route::class); +use Symfony\Contracts\Service\ServiceSubscriberInterface; /** * This Router creates the Loader only when the cache is empty. @@ -39,13 +34,13 @@ class_exists(Route::class); class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberInterface { private $container; - private $collectedParameters = []; - private $paramFetcher; + private array $collectedParameters = []; + private \Closure $paramFetcher; /** * @param mixed $resource The main resource to load */ - public function __construct(ContainerInterface $container, $resource, array $options = [], RequestContext $context = null, ContainerInterface $parameters = null, LoggerInterface $logger = null, string $defaultLocale = null) + public function __construct(ContainerInterface $container, mixed $resource, array $options = [], RequestContext $context = null, ContainerInterface $parameters = null, LoggerInterface $logger = null, string $defaultLocale = null) { $this->container = $container; $this->resource = $resource; @@ -54,9 +49,9 @@ public function __construct(ContainerInterface $container, $resource, array $opt $this->setOptions($options); if ($parameters) { - $this->paramFetcher = [$parameters, 'get']; + $this->paramFetcher = \Closure::fromCallable([$parameters, 'get']); } elseif ($container instanceof SymfonyContainerInterface) { - $this->paramFetcher = [$container, 'getParameter']; + $this->paramFetcher = \Closure::fromCallable([$container, 'getParameter']); } else { throw new \LogicException(sprintf('You should either pass a "%s" instance or provide the $parameters argument of the "%s" method.', SymfonyContainerInterface::class, __METHOD__)); } @@ -67,7 +62,7 @@ public function __construct(ContainerInterface $container, $resource, array $opt /** * {@inheritdoc} */ - public function getRouteCollection() + public function getRouteCollection(): RouteCollection { if (null === $this->collection) { $this->collection = $this->container->get('routing.loader')->load($this->resource, $this->options['resource_type']); @@ -90,8 +85,10 @@ public function getRouteCollection() /** * {@inheritdoc} + * + * @return string[] A list of classes to preload on PHP 7.4+ */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir): array { $currentDir = $this->getOption('cache_dir'); @@ -101,6 +98,11 @@ public function warmUp($cacheDir) $this->getGenerator(); $this->setOption('cache_dir', $currentDir); + + return [ + $this->getOption('generator_class'), + $this->getOption('matcher_class'), + ]; } /** @@ -128,31 +130,26 @@ private function resolveParameters(RouteCollection $collection) $schemes = []; foreach ($route->getSchemes() as $scheme) { - $schemes = array_merge($schemes, explode('|', $this->resolve($scheme))); + $schemes[] = explode('|', $this->resolve($scheme)); } - $route->setSchemes($schemes); + $route->setSchemes(array_merge([], ...$schemes)); $methods = []; foreach ($route->getMethods() as $method) { - $methods = array_merge($methods, explode('|', $this->resolve($method))); + $methods[] = explode('|', $this->resolve($method)); } - $route->setMethods($methods); + $route->setMethods(array_merge([], ...$methods)); $route->setCondition($this->resolve($route->getCondition())); } } /** - * Recursively replaces placeholders with the service container parameters. - * - * @param mixed $value The source which might contain "%placeholders%" - * - * @return mixed The source with the placeholders replaced by the container - * parameters. Arrays are resolved recursively. + * Recursively replaces %placeholders% with the service container parameters. * * @throws ParameterNotFoundException When a placeholder does not exist as a container parameter * @throws RuntimeException When a container value is not a string or a numeric value */ - private function resolve($value) + private function resolve(mixed $value): mixed { if (\is_array($value)) { foreach ($value as $key => $val) { @@ -190,7 +187,7 @@ private function resolve($value) } } - throw new RuntimeException(sprintf('The container parameter "%s", used in the route configuration value "%s", must be a string or numeric, but it is of type "%s".', $match[1], $value, \gettype($resolved))); + throw new RuntimeException(sprintf('The container parameter "%s", used in the route configuration value "%s", must be a string or numeric, but it is of type "%s".', $match[1], $value, get_debug_type($resolved))); }, $value); return str_replace('%%', '%', $escapedValue); @@ -199,7 +196,7 @@ private function resolve($value) /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'routing.loader' => LoaderInterface::class, diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php index 3c1670feae02d..4fa7c40bf8d72 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php @@ -18,7 +18,7 @@ */ class DotenvVault extends AbstractVault { - private $dotenvFile; + private string $dotenvFile; public function __construct(string $dotenvFile) { @@ -38,7 +38,7 @@ public function seal(string $name, string $value): void $this->validateName($name); $v = str_replace("'", "'\\''", $value); - $content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; + $content = is_file($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; $content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)/m", "$name='$v'", $content, -1, $count); if (!$count) { @@ -70,7 +70,7 @@ public function remove(string $name): bool $this->lastMessage = null; $this->validateName($name); - $content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; + $content = is_file($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; $content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)\n?/m", '', $content, -1, $count); if ($count) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index 9d8f1529b4419..1f8cbff6693f2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -23,21 +23,17 @@ */ class SodiumVault extends AbstractVault implements EnvVarLoaderInterface { - private $encryptionKey; - private $decryptionKey; - private $pathPrefix; - private $secretsDir; + private ?string $encryptionKey = null; + private string|\Stringable|null $decryptionKey = null; + private string $pathPrefix; + private ?string $secretsDir; /** - * @param string|\Stringable|null $decryptionKey A string or a stringable object that defines the private key to use to decrypt the vault - * or null to store generated keys in the provided $secretsDir + * @param $decryptionKey A string or a stringable object that defines the private key to use to decrypt the vault + * or null to store generated keys in the provided $secretsDir */ - public function __construct(string $secretsDir, $decryptionKey = null) + public function __construct(string $secretsDir, string|\Stringable $decryptionKey = null) { - if (null !== $decryptionKey && !\is_string($decryptionKey) && !(\is_object($decryptionKey) && method_exists($decryptionKey, '__toString'))) { - throw new \TypeError(sprintf('Decryption key should be a string or an object that implements the __toString() method, "%s" given.', \gettype($decryptionKey))); - } - $this->pathPrefix = rtrim(strtr($secretsDir, '/', \DIRECTORY_SEPARATOR), \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.basename($secretsDir).'.'; $this->decryptionKey = $decryptionKey; $this->secretsDir = $secretsDir; @@ -59,7 +55,7 @@ public function generateKeys(bool $override = false): bool // ignore failures to load keys } - if ('' !== $this->decryptionKey && !file_exists($this->pathPrefix.'encrypt.public.php')) { + if ('' !== $this->decryptionKey && !is_file($this->pathPrefix.'encrypt.public.php')) { $this->export('encrypt.public', $this->encryptionKey); } @@ -85,7 +81,8 @@ public function seal(string $name, string $value): void $this->lastMessage = null; $this->validateName($name); $this->loadKeys(); - $this->export($name.'.'.substr(md5($name), 0, 6), sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey))); + $filename = $this->getFilename($name); + $this->export($filename, sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey))); $list = $this->list(); $list[$name] = null; @@ -100,7 +97,8 @@ public function reveal(string $name): ?string $this->lastMessage = null; $this->validateName($name); - if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.php', -26))) { + $filename = $this->getFilename($name); + if (!is_file($file = $this->pathPrefix.$filename.'.php')) { $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); return null; @@ -134,7 +132,8 @@ public function remove(string $name): bool $this->lastMessage = null; $this->validateName($name); - if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.php', -26))) { + $filename = $this->getFilename($name); + if (!is_file($file = $this->pathPrefix.$filename.'.php')) { $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); return false; @@ -153,7 +152,7 @@ public function list(bool $reveal = false): array { $this->lastMessage = null; - if (!file_exists($file = $this->pathPrefix.'list.php')) { + if (!is_file($file = $this->pathPrefix.'list.php')) { return []; } @@ -185,11 +184,11 @@ private function loadKeys(): void return; } - if (file_exists($this->pathPrefix.'decrypt.private.php')) { + if (is_file($this->pathPrefix.'decrypt.private.php')) { $this->decryptionKey = (string) include $this->pathPrefix.'decrypt.private.php'; } - if (file_exists($this->pathPrefix.'encrypt.public.php')) { + if (is_file($this->pathPrefix.'encrypt.public.php')) { $this->encryptionKey = (string) include $this->pathPrefix.'encrypt.public.php'; } elseif ('' !== $this->decryptionKey) { $this->encryptionKey = sodium_crypto_box_publickey($this->decryptionKey); @@ -198,15 +197,16 @@ private function loadKeys(): void } } - private function export(string $file, string $data): void + private function export(string $filename, string $data): void { - $name = basename($this->pathPrefix.$file); + $b64 = 'decrypt.private' === $filename ? '// SYMFONY_DECRYPTION_SECRET='.base64_encode($data)."\n" : ''; + $name = basename($this->pathPrefix.$filename); $data = str_replace('%', '\x', rawurlencode($data)); - $data = sprintf("createSecretsDir(); - if (false === file_put_contents($this->pathPrefix.$file.'.php', $data, \LOCK_EX)) { + if (false === file_put_contents($this->pathPrefix.$filename.'.php', $data, \LOCK_EX)) { $e = error_get_last(); throw new \ErrorException($e['message'] ?? 'Failed to write secrets data.', 0, $e['type'] ?? \E_USER_WARNING); } @@ -220,4 +220,10 @@ private function createSecretsDir(): void $this->secretsDir = null; } + + private function getFilename(string $name): string + { + // The MD5 hash allows making secrets case-sensitive. The filename is not enough on Windows. + return $name.'.'.substr(md5($name), 0, 6); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php deleted file mode 100644 index f1d4d8a93c7c8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.DelegatingEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Psr\Container\ContainerInterface; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Templating\DelegatingEngine as BaseDelegatingEngine; - -/** - * DelegatingEngine selects an engine for a given template. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class DelegatingEngine extends BaseDelegatingEngine implements EngineInterface -{ - protected $container; - - public function __construct(ContainerInterface $container, array $engineIds) - { - $this->container = $container; - $this->engines = $engineIds; - } - - /** - * {@inheritdoc} - */ - public function getEngine($name) - { - $this->resolveEngines(); - - return parent::getEngine($name); - } - - /** - * {@inheritdoc} - */ - public function renderResponse($view, array $parameters = [], Response $response = null) - { - $engine = $this->getEngine($view); - - if ($engine instanceof EngineInterface) { - return $engine->renderResponse($view, $parameters, $response); - } - - if (null === $response) { - $response = new Response(); - } - - $response->setContent($engine->render($view, $parameters)); - - return $response; - } - - /** - * Resolved engine ids to their real engine instances from the container. - */ - private function resolveEngines() - { - foreach ($this->engines as $i => $engine) { - if (\is_string($engine)) { - $this->engines[$i] = $this->container->get($engine); - } - } - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php b/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php deleted file mode 100644 index 28b023bb0348a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.EngineInterface::class.' interface is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Templating\EngineInterface as BaseEngineInterface; - -/** - * EngineInterface is the interface each engine must implement. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -interface EngineInterface extends BaseEngineInterface -{ - /** - * Renders a view and returns a Response. - * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * - * @return Response A Response instance - * - * @throws \RuntimeException if the template cannot be rendered - */ - public function renderResponse($view, array $parameters = [], Response $response = null); -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php b/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php deleted file mode 100644 index 29e45ac7f15f9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.GlobalVariables::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - -/** - * GlobalVariables is the entry point for Symfony global variables in PHP templates. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class GlobalVariables -{ - protected $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - /** - * @return TokenInterface|null - */ - public function getToken() - { - if (!$this->container->has('security.token_storage')) { - return null; - } - - return $this->container->get('security.token_storage')->getToken(); - } - - public function getUser() - { - if (!$token = $this->getToken()) { - return null; - } - - $user = $token->getUser(); - - return \is_object($user) ? $user : null; - } - - /** - * @return Request|null The HTTP request object - */ - public function getRequest() - { - return $this->container->has('request_stack') ? $this->container->get('request_stack')->getCurrentRequest() : null; - } - - /** - * @return Session|null The session - */ - public function getSession() - { - $request = $this->getRequest(); - - return $request && $request->hasSession() ? $request->getSession() : null; - } - - /** - * @return string The current environment string (e.g 'dev') - */ - public function getEnvironment() - { - return $this->container->getParameter('kernel.environment'); - } - - /** - * @return bool The current debug mode - */ - public function getDebug() - { - return (bool) $this->container->getParameter('kernel.debug'); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php deleted file mode 100644 index 3e689f5934ead..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.ActionsHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\HttpKernel\Controller\ControllerReference; -use Symfony\Component\HttpKernel\Fragment\FragmentHandler; -use Symfony\Component\Templating\Helper\Helper; - -/** - * ActionsHelper manages action inclusions. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class ActionsHelper extends Helper -{ - private $handler; - - public function __construct(FragmentHandler $handler) - { - $this->handler = $handler; - } - - /** - * Returns the fragment content for a given URI. - * - * @param string $uri - * - * @return string The fragment content - * - * @see FragmentHandler::render() - */ - public function render($uri, array $options = []) - { - $strategy = $options['strategy'] ?? 'inline'; - unset($options['strategy']); - - return $this->handler->render($uri, $strategy, $options); - } - - public function controller($controller, $attributes = [], $query = []) - { - return new ControllerReference($controller, $attributes, $query); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'actions'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php deleted file mode 100644 index 0e326b628d4f9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.AssetsHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Asset\Packages; -use Symfony\Component\Templating\Helper\Helper; - -/** - * AssetsHelper helps manage asset URLs. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class AssetsHelper extends Helper -{ - private $packages; - - public function __construct(Packages $packages) - { - $this->packages = $packages; - } - - /** - * Returns the public url/path of an asset. - * - * If the package used to generate the path is an instance of - * UrlPackage, you will always get a URL and not a path. - * - * @param string $path A public path - * @param string $packageName The name of the asset package to use - * - * @return string The public path of the asset - */ - public function getUrl($path, $packageName = null) - { - return $this->packages->getUrl($path, $packageName); - } - - /** - * Returns the version of an asset. - * - * @param string $path A public path - * @param string $packageName The name of the asset package to use - * - * @return string The asset version - */ - public function getVersion($path, $packageName = null) - { - return $this->packages->getVersion($path, $packageName); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'assets'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php deleted file mode 100644 index 427bd41d0dee8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php +++ /dev/null @@ -1,233 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.CodeHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; -use Symfony\Component\Templating\Helper\Helper; - -/** - * @author Fabien Potencier - * - * @internal since Symfony 4.2 - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class CodeHelper extends Helper -{ - protected $fileLinkFormat; - protected $rootDir; // to be renamed $projectDir in 5.0 - protected $charset; - - /** - * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files - * @param string $projectDir The project root directory - * @param string $charset The charset - */ - public function __construct($fileLinkFormat, string $projectDir, string $charset) - { - $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); - $this->rootDir = str_replace('\\', '/', $projectDir).'/'; - $this->charset = $charset; - } - - /** - * Formats an array as a string. - * - * @param array $args The argument array - * - * @return string - */ - public function formatArgsAsText(array $args) - { - return strip_tags($this->formatArgs($args)); - } - - public function abbrClass($class) - { - $parts = explode('\\', $class); - $short = array_pop($parts); - - return sprintf('%s', $class, $short); - } - - public function abbrMethod($method) - { - if (str_contains($method, '::')) { - [$class, $method] = explode('::', $method, 2); - $result = sprintf('%s::%s()', $this->abbrClass($class), $method); - } elseif ('Closure' === $method) { - $result = sprintf('%1$s', $method); - } else { - $result = sprintf('%1$s()', $method); - } - - return $result; - } - - /** - * Formats an array as a string. - * - * @param array $args The argument array - * - * @return string - */ - public function formatArgs(array $args) - { - $result = []; - foreach ($args as $key => $item) { - if ('object' === $item[0]) { - $parts = explode('\\', $item[1]); - $short = array_pop($parts); - $formattedValue = sprintf('object(%s)', $item[1], $short); - } elseif ('array' === $item[0]) { - $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); - } elseif ('string' === $item[0]) { - $formattedValue = sprintf("'%s'", htmlspecialchars($item[1], \ENT_QUOTES, $this->getCharset())); - } elseif ('null' === $item[0]) { - $formattedValue = 'null'; - } elseif ('boolean' === $item[0]) { - $formattedValue = ''.strtolower(var_export($item[1], true)).''; - } elseif ('resource' === $item[0]) { - $formattedValue = 'resource'; - } else { - $formattedValue = str_replace("\n", '', var_export(htmlspecialchars((string) $item[1], \ENT_QUOTES, $this->getCharset()), true)); - } - - $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); - } - - return implode(', ', $result); - } - - /** - * Returns an excerpt of a code file around the given line number. - * - * @param string $file A file path - * @param int $line The selected line number - * - * @return string|null An HTML string - */ - public function fileExcerpt($file, $line) - { - if (is_readable($file)) { - if (\extension_loaded('fileinfo')) { - $finfo = new \finfo(); - - // Check if the file is an application/octet-stream (eg. Phar file) because highlight_file cannot parse these files - if ('application/octet-stream' === $finfo->file($file, \FILEINFO_MIME_TYPE)) { - return ''; - } - } - - // highlight_file could throw warnings - // see https://bugs.php.net/25725 - $code = @highlight_file($file, true); - // remove main code/span tags - $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); - $content = explode('
', $code); - - $lines = []; - for ($i = max($line - 3, 1), $max = min($line + 3, \count($content)); $i <= $max; ++$i) { - $lines[] = ''.self::fixCodeMarkup($content[$i - 1]).''; - } - - return '
    '.implode("\n", $lines).'
'; - } - - return null; - } - - /** - * Formats a file path. - * - * @param string $file An absolute file path - * @param int $line The line number - * @param string $text Use this text for the link rather than the file path - * - * @return string - */ - public function formatFile($file, $line, $text = null) - { - $flags = \ENT_QUOTES | \ENT_SUBSTITUTE; - - if (null === $text) { - $file = trim($file); - $fileStr = $file; - if (str_starts_with($fileStr, $this->rootDir)) { - $fileStr = str_replace(['\\', $this->rootDir], ['/', ''], $fileStr); - $fileStr = htmlspecialchars($fileStr, $flags, $this->charset); - $fileStr = sprintf('kernel.project_dir/%s', htmlspecialchars($this->rootDir, $flags, $this->charset), $fileStr); - } - - $text = sprintf('%s at line %d', $fileStr, $line); - } - - if (false !== $link = $this->getFileLink($file, $line)) { - return sprintf('%s', htmlspecialchars($link, $flags, $this->charset), $text); - } - - return $text; - } - - /** - * Returns the link for a given file/line pair. - * - * @param string $file An absolute file path - * @param int $line The line number - * - * @return string A link of false - */ - public function getFileLink($file, $line) - { - if ($fmt = $this->fileLinkFormat) { - return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); - } - - return false; - } - - public function formatFileFromText($text) - { - return preg_replace_callback('/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) { - return 'in '.$this->formatFile($match[2], $match[3]); - }, $text); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'code'; - } - - protected static function fixCodeMarkup($line) - { - // ending tag from previous line - $opening = strpos($line, ''); - if (false !== $closing && (false === $opening || $closing < $opening)) { - $line = substr_replace($line, '', $closing, 7); - } - - // missing tag at the end of line - $opening = strpos($line, ''); - if (false !== $opening && (false === $closing || $closing > $opening)) { - $line .= ''; - } - - return $line; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php deleted file mode 100644 index cb18d3eb34406..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ /dev/null @@ -1,260 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.FormHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Form\FormRendererInterface; -use Symfony\Component\Form\FormView; -use Symfony\Component\Templating\Helper\Helper; - -/** - * FormHelper provides helpers to help display forms. - * - * @author Fabien Potencier - * @author Bernhard Schussek - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class FormHelper extends Helper -{ - private $renderer; - - public function __construct(FormRendererInterface $renderer) - { - $this->renderer = $renderer; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'form'; - } - - /** - * Sets a theme for a given view. - * - * The theme format is ":". - * - * @param string|array $themes A theme or an array of theme - * @param bool $useDefaultThemes If true, will use default themes defined in the renderer - */ - public function setTheme(FormView $view, $themes, $useDefaultThemes = true) - { - $this->renderer->setTheme($view, $themes, $useDefaultThemes); - } - - /** - * Renders the HTML for a form. - * - * Example usage: - * - * form($form) ?> - * - * You can pass options during the call: - * - * form($form, ['attr' => ['class' => 'foo']]) ?> - * - * form($form, ['separator' => '+++++']) ?> - * - * This method is mainly intended for prototyping purposes. If you want to - * control the layout of a form in a more fine-grained manner, you are - * advised to use the other helper methods for rendering the parts of the - * form individually. You can also create a custom form theme to adapt - * the look of the form. - * - * @param array $variables Additional variables passed to the template - * - * @return string The HTML markup - */ - public function form(FormView $view, array $variables = []) - { - return $this->renderer->renderBlock($view, 'form', $variables); - } - - /** - * Renders the form start tag. - * - * Example usage templates: - * - * start($form) ?>> - * - * @param array $variables Additional variables passed to the template - * - * @return string The HTML markup - */ - public function start(FormView $view, array $variables = []) - { - return $this->renderer->renderBlock($view, 'form_start', $variables); - } - - /** - * Renders the form end tag. - * - * Example usage templates: - * - * end($form) ?>> - * - * @param array $variables Additional variables passed to the template - * - * @return string The HTML markup - */ - public function end(FormView $view, array $variables = []) - { - return $this->renderer->renderBlock($view, 'form_end', $variables); - } - - /** - * Renders the HTML for a given view. - * - * Example usage: - * - * widget($form) ?> - * - * You can pass options during the call: - * - * widget($form, ['attr' => ['class' => 'foo']]) ?> - * - * widget($form, ['separator' => '+++++']) ?> - * - * @param array $variables Additional variables passed to the template - * - * @return string The HTML markup - */ - public function widget(FormView $view, array $variables = []) - { - return $this->renderer->searchAndRenderBlock($view, 'widget', $variables); - } - - /** - * Renders the entire form field "row". - * - * @param array $variables Additional variables passed to the template - * - * @return string The HTML markup - */ - public function row(FormView $view, array $variables = []) - { - return $this->renderer->searchAndRenderBlock($view, 'row', $variables); - } - - /** - * Renders the label of the given view. - * - * @param string $label The label - * @param array $variables Additional variables passed to the template - * - * @return string The HTML markup - */ - public function label(FormView $view, $label = null, array $variables = []) - { - if (null !== $label) { - $variables += ['label' => $label]; - } - - return $this->renderer->searchAndRenderBlock($view, 'label', $variables); - } - - /** - * Renders the help of the given view. - * - * @return string The HTML markup - */ - public function help(FormView $view): string - { - return $this->renderer->searchAndRenderBlock($view, 'help'); - } - - /** - * Renders the errors of the given view. - * - * @return string The HTML markup - */ - public function errors(FormView $view) - { - return $this->renderer->searchAndRenderBlock($view, 'errors'); - } - - /** - * Renders views which have not already been rendered. - * - * @param array $variables An array of variables - * - * @return string The HTML markup - */ - public function rest(FormView $view, array $variables = []) - { - return $this->renderer->searchAndRenderBlock($view, 'rest', $variables); - } - - /** - * Renders a block of the template. - * - * @param string $blockName The name of the block to render - * @param array $variables The variable to pass to the template - * - * @return string The HTML markup - */ - public function block(FormView $view, $blockName, array $variables = []) - { - return $this->renderer->renderBlock($view, $blockName, $variables); - } - - /** - * Returns a CSRF token. - * - * Use this helper for CSRF protection without the overhead of creating a - * form. - * - * echo $view['form']->csrfToken('rm_user_'.$user->getId()); - * - * Check the token in your action using the same CSRF token id. - * - * // $csrfProvider being an instance of Symfony\Component\Security\Csrf\TokenGenerator\TokenGeneratorInterface - * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) { - * throw new \RuntimeException('CSRF attack detected.'); - * } - * - * @param string $tokenId The CSRF token id of the protected action - * - * @return string A CSRF token - * - * @throws \BadMethodCallException when no CSRF provider was injected in the constructor - */ - public function csrfToken($tokenId) - { - return $this->renderer->renderCsrfToken($tokenId); - } - - public function humanize($text) - { - return $this->renderer->humanize($text); - } - - /** - * @internal - */ - public function formEncodeCurrency($text, $widget = '') - { - if ('UTF-8' === $charset = $this->getCharset()) { - $text = htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); - } else { - $text = htmlentities($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); - $text = iconv('UTF-8', $charset, $text); - $widget = iconv('UTF-8', $charset, $widget); - } - - return str_replace('{{ widget }}', $widget, $text); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php deleted file mode 100644 index 0df1f6761d0f1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.RequestHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Templating\Helper\Helper; - -/** - * RequestHelper provides access to the current request parameters. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class RequestHelper extends Helper -{ - protected $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; - } - - /** - * Returns a parameter from the current request object. - * - * @param string $key The name of the parameter - * @param string $default A default value - * - * @return mixed - * - * @see Request::get() - */ - public function getParameter($key, $default = null) - { - return $this->getRequest()->get($key, $default); - } - - /** - * Returns the locale. - * - * @return string - */ - public function getLocale() - { - return $this->getRequest()->getLocale(); - } - - private function getRequest(): Request - { - if (!$this->requestStack->getCurrentRequest()) { - throw new \LogicException('A Request must be available.'); - } - - return $this->requestStack->getCurrentRequest(); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'request'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php deleted file mode 100644 index ad02bca83929c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.RouterHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Templating\Helper\Helper; - -/** - * RouterHelper manages links between pages in a template context. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class RouterHelper extends Helper -{ - protected $generator; - - public function __construct(UrlGeneratorInterface $router) - { - $this->generator = $router; - } - - /** - * Generates a URL reference (as an absolute or relative path) to the route with the given parameters. - * - * @param string $name The name of the route - * @param mixed $parameters An array of parameters - * @param bool $relative Whether to generate a relative or absolute path - * - * @return string The generated URL reference - * - * @see UrlGeneratorInterface - */ - public function path($name, $parameters = [], $relative = false) - { - return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH); - } - - /** - * Generates a URL reference (as an absolute URL or network path) to the route with the given parameters. - * - * @param string $name The name of the route - * @param mixed $parameters An array of parameters - * @param bool $schemeRelative Whether to omit the scheme in the generated URL reference - * - * @return string The generated URL reference - * - * @see UrlGeneratorInterface - */ - public function url($name, $parameters = [], $schemeRelative = false) - { - return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'router'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php deleted file mode 100644 index 33c0e6cea729a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.SessionHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Symfony\Component\Templating\Helper\Helper; - -/** - * SessionHelper provides read-only access to the session attributes. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class SessionHelper extends Helper -{ - protected $session; - protected $requestStack; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; - } - - /** - * Returns an attribute. - * - * @param string $name The attribute name - * @param mixed $default The default value - * - * @return mixed - */ - public function get($name, $default = null) - { - return $this->getSession()->get($name, $default); - } - - public function getFlash($name, array $default = []) - { - return $this->getSession()->getFlashBag()->get($name, $default); - } - - public function getFlashes() - { - return $this->getSession()->getFlashBag()->all(); - } - - public function hasFlash($name) - { - return $this->getSession()->getFlashBag()->has($name); - } - - private function getSession(): SessionInterface - { - if (null === $this->session) { - if (!$this->requestStack->getMasterRequest()) { - throw new \LogicException('A Request must be available.'); - } - - $this->session = $this->requestStack->getMasterRequest()->getSession(); - } - - return $this->session; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'session'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php deleted file mode 100644 index 57d4f1900d59e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.StopwatchHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Stopwatch\Stopwatch; -use Symfony\Component\Templating\Helper\Helper; - -/** - * StopwatchHelper provides methods time your PHP templates. - * - * @author Wouter J - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class StopwatchHelper extends Helper -{ - private $stopwatch; - - public function __construct(Stopwatch $stopwatch = null) - { - $this->stopwatch = $stopwatch; - } - - public function getName() - { - return 'stopwatch'; - } - - public function __call($method, $arguments = []) - { - if (null === $this->stopwatch) { - return null; - } - - if (method_exists($this->stopwatch, $method)) { - return $this->stopwatch->{$method}(...$arguments); - } - - throw new \BadMethodCallException(sprintf('Method "%s" of Stopwatch does not exist.', $method)); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php deleted file mode 100644 index e33197b832e8d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; - -@trigger_error('The '.TranslatorHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Templating\Helper\Helper; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; -use Symfony\Contracts\Translation\TranslatorInterface; -use Symfony\Contracts\Translation\TranslatorTrait; - -/** - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TranslatorHelper extends Helper -{ - use TranslatorTrait { - getLocale as private; - setLocale as private; - trans as private doTrans; - } - - protected $translator; - - /** - * @param TranslatorInterface|null $translator - */ - public function __construct($translator = null) - { - if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } - $this->translator = $translator; - } - - /** - * @see TranslatorInterface::trans() - */ - public function trans($id, array $parameters = [], $domain = 'messages', $locale = null) - { - if (null === $this->translator) { - return $this->doTrans($id, $parameters, $domain, $locale); - } - - return $this->translator->trans($id, $parameters, $domain, $locale); - } - - /** - * @see TranslatorInterface::transChoice() - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter - */ - public function transChoice($id, $number, array $parameters = [], $domain = 'messages', $locale = null) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if (null === $this->translator) { - return $this->doTrans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } - if ($this->translator instanceof TranslatorInterface) { - return $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } - - return $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'translator'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php deleted file mode 100644 index e060a6d7d2238..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Loader; - -@trigger_error('The '.FilesystemLoader::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Config\FileLocatorInterface; -use Symfony\Component\Templating\Loader\LoaderInterface; -use Symfony\Component\Templating\Storage\FileStorage; -use Symfony\Component\Templating\TemplateReferenceInterface; - -/** - * FilesystemLoader is a loader that read templates from the filesystem. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class FilesystemLoader implements LoaderInterface -{ - protected $locator; - - public function __construct(FileLocatorInterface $locator) - { - $this->locator = $locator; - } - - /** - * {@inheritdoc} - */ - public function load(TemplateReferenceInterface $template) - { - try { - $file = $this->locator->locate($template); - } catch (\InvalidArgumentException $e) { - return false; - } - - return new FileStorage($file); - } - - /** - * {@inheritdoc} - */ - public function isFresh(TemplateReferenceInterface $template, $time) - { - if (false === $storage = $this->load($template)) { - return false; - } - - if (!is_readable((string) $storage)) { - return false; - } - - return filemtime((string) $storage) < $time; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php deleted file mode 100644 index d1c6648268181..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating\Loader; - -@trigger_error('The '.TemplateLocator::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Config\FileLocatorInterface; -use Symfony\Component\Templating\TemplateReferenceInterface; - -/** - * TemplateLocator locates templates in bundles. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplateLocator implements FileLocatorInterface -{ - protected $locator; - protected $cache; - - private $cacheHits = []; - - /** - * @param string $cacheDir The cache path - */ - public function __construct(FileLocatorInterface $locator, string $cacheDir = null) - { - if (null !== $cacheDir && file_exists($cache = $cacheDir.'/templates.php')) { - $this->cache = require $cache; - } - - $this->locator = $locator; - } - - /** - * Returns a full path for a given file. - * - * @return string The full path for the file - */ - protected function getCacheKey($template) - { - return $template->getLogicalName(); - } - - /** - * Returns a full path for a given file. - * - * @param TemplateReferenceInterface $template A template - * @param string $currentPath Unused - * @param bool $first Unused - * - * @return string The full path for the file - * - * @throws \InvalidArgumentException When the template is not an instance of TemplateReferenceInterface - * @throws \InvalidArgumentException When the template file can not be found - */ - public function locate($template, $currentPath = null, $first = true) - { - if (!$template instanceof TemplateReferenceInterface) { - throw new \InvalidArgumentException('The template must be an instance of TemplateReferenceInterface.'); - } - - $key = $this->getCacheKey($template); - - if (isset($this->cacheHits[$key])) { - return $this->cacheHits[$key]; - } - if (isset($this->cache[$key])) { - return $this->cacheHits[$key] = realpath($this->cache[$key]) ?: $this->cache[$key]; - } - - try { - return $this->cacheHits[$key] = $this->locator->locate($template->getPath(), $currentPath); - } catch (\InvalidArgumentException $e) { - throw new \InvalidArgumentException(sprintf('Unable to find template "%s": ', $template).$e->getMessage(), 0, $e); - } - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php deleted file mode 100644 index f1f9347b7b807..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.PhpEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Psr\Container\ContainerInterface; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Templating\Loader\LoaderInterface; -use Symfony\Component\Templating\PhpEngine as BasePhpEngine; -use Symfony\Component\Templating\TemplateNameParserInterface; - -/** - * This engine knows how to render Symfony templates. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class PhpEngine extends BasePhpEngine implements EngineInterface -{ - protected $container; - - public function __construct(TemplateNameParserInterface $parser, ContainerInterface $container, LoaderInterface $loader, GlobalVariables $globals = null) - { - $this->container = $container; - - parent::__construct($parser, $loader); - - if (null !== $globals) { - $this->addGlobal('app', $globals); - } - } - - /** - * {@inheritdoc} - */ - public function get($name) - { - if (!isset($this->helpers[$name])) { - throw new \InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); - } - - if (\is_string($this->helpers[$name])) { - $this->helpers[$name] = $this->container->get($this->helpers[$name]); - $this->helpers[$name]->setCharset($this->charset); - } - - return $this->helpers[$name]; - } - - /** - * {@inheritdoc} - */ - public function setHelpers(array $helpers) - { - $this->helpers = $helpers; - } - - /** - * {@inheritdoc} - */ - public function renderResponse($view, array $parameters = [], Response $response = null) - { - if (null === $response) { - $response = new Response(); - } - - $response->setContent($this->render($view, $parameters)); - - return $response; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php deleted file mode 100644 index b7c56cefb1754..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.TemplateFilenameParser::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReferenceInterface; - -/** - * TemplateFilenameParser converts template filenames to - * TemplateReferenceInterface instances. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplateFilenameParser implements TemplateNameParserInterface -{ - /** - * {@inheritdoc} - */ - public function parse($name) - { - if ($name instanceof TemplateReferenceInterface) { - return $name; - } - - $parts = explode('/', str_replace('\\', '/', $name)); - - $elements = explode('.', array_pop($parts)); - if (3 > \count($elements)) { - return false; - } - $engine = array_pop($elements); - $format = array_pop($elements); - - return new TemplateReference('', implode('/', $parts), implode('.', $elements), $format, $engine); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php deleted file mode 100644 index e553b85f03ffa..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.TemplateNameParser::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Templating\TemplateNameParser as BaseTemplateNameParser; -use Symfony\Component\Templating\TemplateReferenceInterface; - -/** - * TemplateNameParser converts template names from the short notation - * "bundle:section:template.format.engine" to TemplateReferenceInterface - * instances. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplateNameParser extends BaseTemplateNameParser -{ - protected $kernel; - protected $cache = []; - - public function __construct(KernelInterface $kernel) - { - $this->kernel = $kernel; - } - - /** - * {@inheritdoc} - */ - public function parse($name) - { - if ($name instanceof TemplateReferenceInterface) { - return $name; - } elseif (isset($this->cache[$name])) { - return $this->cache[$name]; - } - - // normalize name - $name = preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name)); - - if (str_contains($name, '..')) { - throw new \RuntimeException(sprintf('Template name "%s" contains invalid characters.', $name)); - } - - if (!preg_match('/^(?:([^:]*):([^:]*):)?(.+)\.([^\.]+)\.([^\.]+)$/', $name, $matches) || str_starts_with($name, '@')) { - return parent::parse($name); - } - - $template = new TemplateReference($matches[1], $matches[2], $matches[3], $matches[4], $matches[5]); - - if ($template->get('bundle')) { - try { - $this->kernel->getBundle($template->get('bundle')); - } catch (\Exception $e) { - throw new \InvalidArgumentException(sprintf('Template name "%s" is not valid.', $name), 0, $e); - } - } - - return $this->cache[$name] = $template; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php deleted file mode 100644 index 4bf4778a801b7..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.TemplateReference::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Templating\TemplateReference as BaseTemplateReference; - -/** - * Internal representation of a template. - * - * @author Victor Berchet - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TemplateReference extends BaseTemplateReference -{ - public function __construct(string $bundle = null, string $controller = null, string $name = null, string $format = null, string $engine = null) - { - $this->parameters = [ - 'bundle' => $bundle, - 'controller' => $controller, - 'name' => $name, - 'format' => $format, - 'engine' => $engine, - ]; - } - - /** - * Returns the path to the template - * - as a path when the template is not part of a bundle - * - as a resource when the template is part of a bundle. - * - * @return string A path to the template or a resource - */ - public function getPath() - { - $controller = str_replace('\\', '/', $this->get('controller')); - - $path = (empty($controller) ? '' : $controller.'/').$this->get('name').'.'.$this->get('format').'.'.$this->get('engine'); - - return empty($this->parameters['bundle']) ? 'views/'.$path : '@'.$this->get('bundle').'/Resources/views/'.$path; - } - - /** - * {@inheritdoc} - */ - public function getLogicalName() - { - return sprintf('%s:%s:%s.%s.%s', $this->parameters['bundle'], $this->parameters['controller'], $this->parameters['name'], $this->parameters['format'], $this->parameters['engine']); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php deleted file mode 100644 index 76509a8f16a7a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Templating; - -@trigger_error('The '.TimedPhpEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Psr\Container\ContainerInterface; -use Symfony\Component\Stopwatch\Stopwatch; -use Symfony\Component\Templating\Loader\LoaderInterface; -use Symfony\Component\Templating\TemplateNameParserInterface; - -/** - * Times the time spent to render a template. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class TimedPhpEngine extends PhpEngine -{ - protected $stopwatch; - - public function __construct(TemplateNameParserInterface $parser, ContainerInterface $container, LoaderInterface $loader, Stopwatch $stopwatch, GlobalVariables $globals = null) - { - parent::__construct($parser, $container, $loader, $globals); - - $this->stopwatch = $stopwatch; - } - - /** - * {@inheritdoc} - */ - public function render($name, array $parameters = []) - { - $e = $this->stopwatch->start(sprintf('template.php (%s)', $name), 'template'); - - $ret = parent::render($name, $parameters); - - $e->stop(); - - return $ret; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php index 086d83e8adf0c..55b95f055994f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Test; +use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\LogicalAnd; use PHPUnit\Framework\Constraint\LogicalNot; +use PHPUnit\Framework\ExpectationFailedException; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint; use Symfony\Component\HttpFoundation\Request; @@ -28,12 +30,17 @@ trait BrowserKitAssertionsTrait { public static function assertResponseIsSuccessful(string $message = ''): void { - self::assertThat(self::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful(), $message); } public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void { - self::assertThat(self::getResponse(), new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message); + } + + public static function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat), $message); } public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void @@ -46,60 +53,65 @@ public static function assertResponseRedirects(string $expectedLocation = null, $constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode)); } - self::assertThat(self::getResponse(), $constraint, $message); + self::assertThatForResponse($constraint, $message); } public static function assertResponseHasHeader(string $headerName, string $message = ''): void { - self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseHasHeader($headerName), $message); } public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void { - self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message); + self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message); } public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void { - self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message); } public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void { - self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message); + self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message); } public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message); } public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message); + self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message); } public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getResponse(), LogicalAnd::fromConstraints( + self::assertThatForResponse(LogicalAnd::fromConstraints( new ResponseConstraint\ResponseHasCookie($name, $path, $domain), new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain) ), $message); } + public static function assertResponseIsUnprocessable(string $message = ''): void + { + self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable(), $message); + } + public static function assertBrowserHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getClient(), new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message); + self::assertThatForClient(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), $message); } public static function assertBrowserNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getClient(), new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message); + self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message); } public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', string $domain = null, string $message = ''): void { - self::assertThat(self::getClient(), LogicalAnd::fromConstraints( + self::assertThatForClient(LogicalAnd::fromConstraints( new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain), new BrowserKitConstraint\BrowserCookieValueSame($name, $expectedValue, $raw, $path, $domain) ), $message); @@ -110,7 +122,7 @@ public static function assertRequestAttributeValueSame(string $name, string $exp self::assertThat(self::getRequest(), new ResponseConstraint\RequestAttributeValueSame($name, $expectedValue), $message); } - public static function assertRouteSame($expectedRoute, array $parameters = [], string $message = ''): void + public static function assertRouteSame(string $expectedRoute, array $parameters = [], string $message = ''): void { $constraint = new ResponseConstraint\RequestAttributeValueSame('_route', $expectedRoute); $constraints = []; @@ -124,6 +136,26 @@ public static function assertRouteSame($expectedRoute, array $parameters = [], s self::assertThat(self::getRequest(), $constraint, $message); } + public static function assertThatForResponse(Constraint $constraint, string $message = ''): void + { + try { + self::assertThat(self::getResponse(), $constraint, $message); + } catch (ExpectationFailedException $exception) { + if (($serverExceptionMessage = self::getResponse()->headers->get('X-Debug-Exception')) + && ($serverExceptionFile = self::getResponse()->headers->get('X-Debug-Exception-File'))) { + $serverExceptionFile = explode(':', $serverExceptionFile); + $exception->__construct($exception->getMessage(), $exception->getComparisonFailure(), new \ErrorException(rawurldecode($serverExceptionMessage), 0, 1, rawurldecode($serverExceptionFile[0]), $serverExceptionFile[1]), $exception->getPrevious()); + } + + throw $exception; + } + } + + public static function assertThatForClient(Constraint $constraint, string $message = ''): void + { + self::assertThat(self::getClient(), $constraint, $message); + } + private static function getClient(AbstractBrowser $newClient = null): ?AbstractBrowser { static $client; diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php index 465c265f6921d..2a692d6f5a367 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Test\Constraint as DomCrawlerConstraint; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorAttributeValueSame; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorExists; /** * Ideas borrowed from Laravel Dusk's assertions. @@ -83,6 +85,39 @@ public static function assertInputValueNotSame(string $fieldName, string $expect ), $message); } + public static function assertCheckboxChecked(string $fieldName, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'checked', 'checked') + ), $message); + } + + public static function assertCheckboxNotChecked(string $fieldName, string $message = ''): void + { + self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints( + new CrawlerSelectorExists("input[name=\"$fieldName\"]"), + new LogicalNot(new CrawlerSelectorAttributeValueSame("input[name=\"$fieldName\"]", 'checked', 'checked')) + ), $message); + } + + public static function assertFormValue(string $formSelector, string $fieldName, string $value, string $message = ''): void + { + $node = self::getCrawler()->filter($formSelector); + self::assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector)); + $values = $node->form()->getValues(); + self::assertArrayHasKey($fieldName, $values, $message ?: sprintf('Field "%s" not found in form "%s".', $fieldName, $formSelector)); + self::assertSame($value, $values[$fieldName]); + } + + public static function assertNoFormValue(string $formSelector, string $fieldName, string $message = ''): void + { + $node = self::getCrawler()->filter($formSelector); + self::assertNotEmpty($node, sprintf('Form "%s" not found.', $formSelector)); + $values = $node->form()->getValues(); + self::assertArrayNotHasKey($fieldName, $values, $message ?: sprintf('Field "%s" has a value in form "%s".', $fieldName, $formSelector)); + } + private static function getCrawler(): Crawler { if (!$crawler = self::getClient()->getCrawler()) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php deleted file mode 100644 index 9a0dad403063c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Test; - -use PHPUnit\Framework\TestCase; - -// Auto-adapt to PHPUnit 8 that added a `void` return-type to the setUp/tearDown methods - -if ((new \ReflectionMethod(TestCase::class, 'tearDown'))->hasReturnType()) { - /** - * @internal - */ - trait ForwardCompatTestTrait - { - private function doSetUp(): void - { - } - - private function doTearDown(): void - { - } - - protected function setUp(): void - { - $this->doSetUp(); - } - - protected function tearDown(): void - { - $this->doTearDown(); - } - } -} else { - /** - * @internal - */ - trait ForwardCompatTestTrait - { - /** - * @return void - */ - private function doSetUp() - { - } - - /** - * @return void - */ - private function doTearDown() - { - } - - /** - * @return void - */ - protected function setUp() - { - $this->doSetUp(); - } - - /** - * @return void - */ - protected function tearDown() - { - $this->doTearDown(); - } - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index 1ec6548be2738..822b35ea75d57 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Contracts\Service\ResetInterface; @@ -23,7 +24,7 @@ */ abstract class KernelTestCase extends TestCase { - use ForwardCompatTestTrait; + use MailerAssertionsTrait; protected static $class; @@ -32,14 +33,9 @@ abstract class KernelTestCase extends TestCase */ protected static $kernel; - /** - * @var ContainerInterface - */ - protected static $container; - protected static $booted = false; - private function doTearDown() + protected function tearDown(): void { static::ensureKernelShutdown(); static::$class = null; @@ -48,12 +44,10 @@ private function doTearDown() } /** - * @return string The Kernel class name - * * @throws \RuntimeException * @throws \LogicException */ - protected static function getKernelClass() + protected static function getKernelClass(): string { if (!isset($_SERVER['KERNEL_CLASS']) && !isset($_ENV['KERNEL_CLASS'])) { throw new \LogicException(sprintf('You must set the KERNEL_CLASS environment variable to the fully-qualified class name of your Kernel in phpunit.xml / phpunit.xml.dist or override the "%1$s::createKernel()" or "%1$s::getKernelClass()" method.', static::class)); @@ -68,10 +62,8 @@ protected static function getKernelClass() /** * Boots the Kernel for this test. - * - * @return KernelInterface A KernelInterface instance */ - protected static function bootKernel(array $options = []) + protected static function bootKernel(array $options = []): KernelInterface { static::ensureKernelShutdown(); @@ -80,12 +72,30 @@ protected static function bootKernel(array $options = []) self::$kernel = $kernel; static::$booted = true; - $container = static::$kernel->getContainer(); - static::$container = $container->has('test.service_container') ? $container->get('test.service_container') : $container; - return static::$kernel; } + /** + * Provides a dedicated test container with access to both public and private + * services. The container will not include private services that have been + * inlined or removed. Private services will be removed when they are not + * used by other services. + * + * Using this method is the best way to get a container from your test code. + */ + protected static function getContainer(): ContainerInterface + { + if (!static::$booted) { + static::bootKernel(); + } + + try { + return self::$kernel->getContainer()->get('test.service_container'); + } catch (ServiceNotFoundException $e) { + throw new \LogicException('Could not find service "test.service_container". Try updating the "framework.test" config to "true".', 0, $e); + } + } + /** * Creates a Kernel. * @@ -93,10 +103,8 @@ protected static function bootKernel(array $options = []) * * * environment * * debug - * - * @return KernelInterface A KernelInterface instance */ - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { if (null === static::$class) { static::$class = static::getKernelClass(); @@ -140,7 +148,5 @@ protected static function ensureKernelShutdown() $container->reset(); } } - - static::$container = null; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php index d0ca84e25ae7a..875c84d4813da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php @@ -118,10 +118,15 @@ public static function getMailerMessage(int $index = 0, string $transport = null private static function getMessageMailerEvents(): MessageEvents { - if (!self::$container->has('mailer.logger_message_listener')) { - static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); + $container = static::getContainer(); + if ($container->has('mailer.message_logger_listener')) { + return $container->get('mailer.message_logger_listener')->getEvents(); } - return self::$container->get('mailer.logger_message_listener')->getEvents(); + if ($container->has('mailer.logger_message_listener')) { + return $container->get('mailer.logger_message_listener')->getEvents(); + } + + static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php new file mode 100644 index 0000000000000..8bf365eb06380 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Test; + +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * A very limited token that is used to login in tests using the KernelBrowser. + * + * @author Wouter de Jong + */ +class TestBrowserToken extends AbstractToken +{ + private string $firewallName; + + public function __construct(array $roles = [], UserInterface $user = null, string $firewallName = 'main') + { + parent::__construct($roles); + + if (null !== $user) { + $this->setUser($user); + } + + $this->firewallName = $firewallName; + } + + public function getFirewallName(): string + { + return $this->firewallName; + } + + public function getCredentials(): mixed + { + return null; + } + + public function __serialize(): array + { + return [$this->firewallName, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->firewallName, $parentData] = $data; + + parent::__unserialize($parentData); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php index d5aec6830317e..4b5ede1eaf6d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php @@ -17,6 +17,11 @@ use Symfony\Component\HttpKernel\KernelInterface; /** + * A special container used in tests. This gives access to both public and + * private services. The container will not include private services that has + * been inlined or removed. Private services will be removed when they are not + * used by other services. + * * @author Nicolas Grekas * * @internal @@ -24,7 +29,7 @@ class TestContainer extends Container { private $kernel; - private $privateServicesLocatorId; + private string $privateServicesLocatorId; public function __construct(KernelInterface $kernel, string $privateServicesLocatorId) { @@ -58,10 +63,8 @@ public function getParameterBag(): ParameterBagInterface /** * {@inheritdoc} - * - * @return array|bool|float|int|string|\UnitEnum|null */ - public function getParameter($name) + public function getParameter(string $name): array|bool|string|int|float|\UnitEnum|null { return $this->getPublicContainer()->getParameter($name); } @@ -69,7 +72,7 @@ public function getParameter($name) /** * {@inheritdoc} */ - public function hasParameter($name): bool + public function hasParameter(string $name): bool { return $this->getPublicContainer()->hasParameter($name); } @@ -77,7 +80,7 @@ public function hasParameter($name): bool /** * {@inheritdoc} */ - public function setParameter($name, $value) + public function setParameter(string $name, mixed $value) { $this->getPublicContainer()->setParameter($name, $value); } @@ -85,7 +88,7 @@ public function setParameter($name, $value) /** * {@inheritdoc} */ - public function set($id, $service) + public function set(string $id, mixed $service) { $this->getPublicContainer()->set($id, $service); } @@ -93,17 +96,15 @@ public function set($id, $service) /** * {@inheritdoc} */ - public function has($id): bool + public function has(string $id): bool { return $this->getPublicContainer()->has($id) || $this->getPrivateContainer()->has($id); } /** * {@inheritdoc} - * - * @return object|null */ - public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERENCE */ 1) + public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object { return $this->getPrivateContainer()->has($id) ? $this->getPrivateContainer()->get($id) : $this->getPublicContainer()->get($id, $invalidBehavior); } @@ -111,7 +112,7 @@ public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERE /** * {@inheritdoc} */ - public function initialized($id): bool + public function initialized(string $id): bool { return $this->getPublicContainer()->initialized($id); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index 19da82c83d0e3..1f7f0802d3ce1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -21,11 +21,9 @@ */ abstract class WebTestCase extends KernelTestCase { - use ForwardCompatTestTrait; - use MailerAssertionsTrait; use WebTestAssertionsTrait; - private function doTearDown() + protected function tearDown(): void { parent::tearDown(); self::getClient(null); @@ -36,13 +34,11 @@ private function doTearDown() * * @param array $options An array of options to pass to the createKernel method * @param array $server An array of server parameters - * - * @return KernelBrowser A KernelBrowser instance */ - protected static function createClient(array $options = [], array $server = []) + protected static function createClient(array $options = [], array $server = []): KernelBrowser { if (static::$booted) { - @trigger_error(sprintf('Calling "%s()" while a kernel has been booted is deprecated since Symfony 4.4 and will throw an exception in 5.0, ensure the kernel is shut down before calling the method.', __METHOD__), \E_USER_DEPRECATED); + throw new \LogicException(sprintf('Booting the kernel before calling "%s()" is not supported, the kernel should only be booted once.', __METHOD__)); } $kernel = static::bootKernel($options); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php index d0eb678420f44..8253c525df8f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php @@ -3,7 +3,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use PHPUnit\Framework\MockObject\MockObject; @@ -12,7 +11,6 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Cache\DoctrineProvider; use Symfony\Component\Filesystem\Filesystem; class AnnotationsCacheWarmerTest extends TestCase @@ -44,16 +42,10 @@ public function testAnnotationsCacheWarmerWithDebugDisabled() $this->assertFileExists($cacheFile); // Assert cache is valid - $reader = class_exists(PsrCachedReader::class) - ? new PsrCachedReader( - $this->getReadOnlyReader(), - new PhpArrayAdapter($cacheFile, new NullAdapter()) - ) - : new CachedReader( - $this->getReadOnlyReader(), - new DoctrineProvider(new PhpArrayAdapter($cacheFile, new NullAdapter())) - ) - ; + $reader = new PsrCachedReader( + $this->getReadOnlyReader(), + new PhpArrayAdapter($cacheFile, new NullAdapter()) + ); $refClass = new \ReflectionClass($this); $reader->getClassAnnotations($refClass); $reader->getMethodAnnotations($refClass->getMethod(__FUNCTION__)); @@ -71,18 +63,11 @@ public function testAnnotationsCacheWarmerWithDebugEnabled() // Assert cache is valid $phpArrayAdapter = new PhpArrayAdapter($cacheFile, new NullAdapter()); - $reader = class_exists(PsrCachedReader::class) - ? new PsrCachedReader( - $this->getReadOnlyReader(), - $phpArrayAdapter, - true - ) - : new CachedReader( - $this->getReadOnlyReader(), - new DoctrineProvider($phpArrayAdapter), - true - ) - ; + $reader = new PsrCachedReader( + $this->getReadOnlyReader(), + $phpArrayAdapter, + true + ); $refClass = new \ReflectionClass($this); $reader->getClassAnnotations($refClass); $reader->getMethodAnnotations($refClass->getMethod(__FUNCTION__)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php index 93dbd1a69e220..61214b039c64a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/RouterCacheWarmerTest.php @@ -28,14 +28,10 @@ public function testWarmUpWithWarmebleInterface() $routerCacheWarmer = new RouterCacheWarmer($containerMock); $routerCacheWarmer->warmUp('/tmp'); - $routerMock->expects($this->any())->method('warmUp')->with('/tmp')->willReturn(''); + $routerMock->expects($this->any())->method('warmUp')->with('/tmp')->willReturn([]); $this->addToAssertionCount(1); } - /** - * @expectedDeprecation Passing a Symfony\Component\Routing\RouterInterface without implementing Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface is deprecated since Symfony 4.1. - * @group legacy - */ public function testWarmUpWithoutWarmebleInterface() { $containerMock = $this->getMockBuilder(ContainerInterface::class)->setMethods(['get', 'has'])->getMock(); @@ -43,6 +39,8 @@ public function testWarmUpWithoutWarmebleInterface() $routerMock = $this->getMockBuilder(testRouterInterfaceWithoutWarmebleInterface::class)->setMethods(['match', 'generate', 'getContext', 'setContext', 'getRouteCollection'])->getMock(); $containerMock->expects($this->any())->method('get')->with('router')->willReturn($routerMock); $routerCacheWarmer = new RouterCacheWarmer($containerMock); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('cannot be warmed up because it does not implement "Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface"'); $routerCacheWarmer->warmUp('/tmp'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php index 18eebf21e66b0..e64196f64d5c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php @@ -15,18 +15,17 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; class SerializerCacheWarmerTest extends TestCase { - public function testWarmUp() + /** + * @dataProvider loaderProvider + */ + public function testWarmUp(array $loaders) { - $loaders = [ - new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), - new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), - ]; - $file = sys_get_temp_dir().'/cache-serializer.php'; @unlink($file); @@ -41,6 +40,26 @@ public function testWarmUp() $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author')->isHit()); } + public function loaderProvider() + { + return [ + [ + [ + new LoaderChain([ + new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), + new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), + ]), + ], + ], + [ + [ + new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), + new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), + ], + ], + ]; + } + public function testWarmUpWithoutLoader() { $file = sys_get_temp_dir().'/cache-serializer-without-loader.php'; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php deleted file mode 100644 index b81c9294259de..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; - -use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinder; -use Symfony\Bundle\FrameworkBundle\Templating\TemplateFilenameParser; -use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BaseBundle\BaseBundle; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\HttpKernel\Kernel; - -/** - * @group legacy - */ -class TemplateFinderTest extends TestCase -{ - public function testFindAllTemplates() - { - $kernel = $this->createMock(Kernel::class); - $kernel - ->expects($this->any()) - ->method('getBundle') - ; - - $kernel - ->expects($this->once()) - ->method('getBundles') - ->willReturn(['BaseBundle' => new BaseBundle()]) - ; - - $parser = new TemplateFilenameParser(); - - $finder = new TemplateFinder($kernel, $parser, __DIR__.'/../Fixtures/Resources'); - - $templates = array_map( - function ($template) { return $template->getLogicalName(); }, - $finder->findAllTemplates() - ); - - $this->assertCount(7, $templates, '->findAllTemplates() find all templates in the bundles and global folders'); - $this->assertContains('BaseBundle::base.format.engine', $templates); - $this->assertContains('BaseBundle::this.is.a.template.format.engine', $templates); - $this->assertContains('BaseBundle:controller:base.format.engine', $templates); - $this->assertContains('BaseBundle:controller:custom.format.engine', $templates); - $this->assertContains('::this.is.a.template.format.engine', $templates); - $this->assertContains('::resource.format.engine', $templates); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php deleted file mode 100644 index f4853162d2f09..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php +++ /dev/null @@ -1,105 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; - -use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinderInterface; -use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplatePathsCacheWarmer; -use Symfony\Bundle\FrameworkBundle\Templating\Loader\TemplateLocator; -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\Filesystem\Filesystem; - -/** - * @group legacy - */ -class TemplatePathsCacheWarmerTest extends TestCase -{ - /** @var Filesystem */ - private $filesystem; - - /** @var TemplateFinderInterface */ - private $templateFinder; - - /** @var FileLocator */ - private $fileLocator; - - /** @var TemplateLocator */ - private $templateLocator; - - private $tmpDir; - - protected function setUp(): void - { - $this->templateFinder = $this - ->getMockBuilder(TemplateFinderInterface::class) - ->setMethods(['findAllTemplates']) - ->getMock(); - - $this->fileLocator = $this - ->getMockBuilder(FileLocator::class) - ->setMethods(['locate']) - ->setConstructorArgs(['/path/to/fallback']) - ->getMock(); - - $this->templateLocator = new TemplateLocator($this->fileLocator); - - $this->tmpDir = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('cache_template_paths_', true); - - $this->filesystem = new Filesystem(); - $this->filesystem->mkdir($this->tmpDir); - } - - protected function tearDown(): void - { - $this->filesystem->remove($this->tmpDir); - } - - public function testWarmUp() - { - $template = new TemplateReference('bundle', 'controller', 'name', 'format', 'engine'); - - $this->templateFinder - ->expects($this->once()) - ->method('findAllTemplates') - ->willReturn([$template]); - - $this->fileLocator - ->expects($this->once()) - ->method('locate') - ->with($template->getPath()) - ->willReturn(\dirname($this->tmpDir).'/path/to/template.html.twig'); - - $warmer = new TemplatePathsCacheWarmer($this->templateFinder, $this->templateLocator); - $warmer->warmUp($this->tmpDir); - - $this->assertFileEquals(__DIR__.'/../Fixtures/TemplatePathsCache/templates.php', $this->tmpDir.'/templates.php'); - } - - public function testWarmUpEmpty() - { - $this->templateFinder - ->expects($this->once()) - ->method('findAllTemplates') - ->willReturn([]); - - $this->fileLocator - ->expects($this->never()) - ->method('locate'); - - $warmer = new TemplatePathsCacheWarmer($this->templateFinder, $this->templateLocator); - $warmer->warmUp($this->tmpDir); - - $this->assertFileExists($this->tmpDir.'/templates.php'); - $this->assertSame(file_get_contents(__DIR__.'/../Fixtures/TemplatePathsCache/templates-empty.php'), file_get_contents($this->tmpDir.'/templates.php')); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php index 92ef379b1b819..8a54680c0f557 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php @@ -26,7 +26,7 @@ public function testWarmUp() $validatorBuilder->addXmlMapping(__DIR__.'/../Fixtures/Validation/Resources/person.xml'); $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/author.yml'); $validatorBuilder->addMethodMapping('loadValidatorMetadata'); - $validatorBuilder->enableAnnotationMapping(); + $validatorBuilder->enableAnnotationMapping(true)->addDefaultDoctrineAnnotationReader(); $file = sys_get_temp_dir().'/cache-validator.php'; @unlink($file); @@ -46,7 +46,7 @@ public function testWarmUpWithAnnotations() { $validatorBuilder = new ValidatorBuilder(); $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/categories.yml'); - $validatorBuilder->enableAnnotationMapping(); + $validatorBuilder->enableAnnotationMapping(true)->addDefaultDoctrineAnnotationReader(); $file = sys_get_temp_dir().'/cache-validator-with-annotations.php'; @unlink($file); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php index 27f2973d82c3d..8a5a023e3b7ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -60,9 +61,12 @@ public function testCacheIsFreshAfterCacheClearedWithWarmup() $configCacheFactory = new ConfigCacheFactory(true); foreach ($metaFiles as $file) { - $configCacheFactory->cache(substr($file, 0, -5), function () use ($file) { - $this->fail(sprintf('Meta file "%s" is not fresh', (string) $file)); - }); + $configCacheFactory->cache( + substr($file, 0, -5), + function () use ($file) { + $this->fail(sprintf('Meta file "%s" is not fresh', (string) $file)); + } + ); } // check that app kernel file present in meta file of container's cache @@ -84,7 +88,51 @@ public function testCacheIsFreshAfterCacheClearedWithWarmup() $this->assertTrue($found, 'Kernel file should present as resource'); $containerRef = new \ReflectionClass(require $containerFile); - $containerFile = str_replace('tes_'.\DIRECTORY_SEPARATOR, 'test'.\DIRECTORY_SEPARATOR, $containerRef->getFileName()); - $this->assertMatchesRegularExpression(sprintf('/\'kernel.container_class\'\s*=>\s*\'%s\'/', $containerClass), file_get_contents($containerFile), 'kernel.container_class is properly set on the dumped container'); + $containerFile = str_replace( + 'tes_'.\DIRECTORY_SEPARATOR, + 'test'.\DIRECTORY_SEPARATOR, + $containerRef->getFileName() + ); + $this->assertMatchesRegularExpression( + sprintf('/\'kernel.container_class\'\s*=>\s*\'%s\'/', $containerClass), + file_get_contents($containerFile), + 'kernel.container_class is properly set on the dumped container' + ); + } + + public function testCacheIsWarmedWhenCalledTwice() + { + $input = new ArrayInput(['cache:clear']); + $application = new Application(clone $this->kernel); + $application->setCatchExceptions(false); + $application->doRun($input, new NullOutput()); + + $_SERVER['REQUEST_TIME'] = time() + 1; + $application = new Application(clone $this->kernel); + $application->setCatchExceptions(false); + $application->doRun($input, new NullOutput()); + + $this->assertTrue(is_file($this->kernel->getCacheDir().'/annotations.php')); + } + + public function testCacheIsWarmedWithOldContainer() + { + $kernel = clone $this->kernel; + + // Hack to get a dumped working container, + // BUT without "kernel.build_dir" parameter (like an old dumped container) + $kernel->boot(); + $container = $kernel->getContainer(); + \Closure::bind(function (Container $class) { + unset($class->loadedDynamicParameters['kernel.build_dir']); + unset($class->parameters['kernel.build_dir']); + }, null, \get_class($container))($container); + + $input = new ArrayInput(['cache:clear']); + $application = new Application($kernel); + $application->setCatchExceptions(false); + $application->doRun($input, new NullOutput()); + + $this->expectNotToPerformAssertions(); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php index d6a58798c1369..678d573586f3a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php @@ -36,13 +36,6 @@ public function registerContainerConfiguration(LoaderInterface $loader) $loader->load(__DIR__.\DIRECTORY_SEPARATOR.'config.yml'); } - public function setAnnotatedClassCache(array $annotatedClasses) - { - $annotatedClasses = array_diff($annotatedClasses, ['Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\PreviewErrorController']); - - parent::setAnnotatedClassCache($annotatedClasses); - } - protected function build(ContainerBuilder $container) { $container->register('logger', NullLogger::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php new file mode 100644 index 0000000000000..169fcd8c2d75d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolClearCommandTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Console\Tester\CommandCompletionTester; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; +use Symfony\Component\HttpKernel\KernelInterface; + +class CachePoolClearCommandTest extends TestCase +{ + private $cachePool; + + protected function setUp(): void + { + $this->cachePool = $this->createMock(CacheItemPoolInterface::class); + } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $application = new Application($this->getKernel()); + $application->add(new CachePoolClearCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); + $tester = new CommandCompletionTester($application->get('cache:pool:clear')); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'pool_name' => [ + ['f'], + ['foo'], + ]; + } + + /** + * @return MockObject&KernelInterface + */ + private function getKernel(): KernelInterface + { + $container = $this->createMock(ContainerInterface::class); + + $kernel = $this->createMock(KernelInterface::class); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container); + + $kernel + ->expects($this->once()) + ->method('getBundles') + ->willReturn([]); + + return $kernel; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php index b840a538cc670..f643bc1259901 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php @@ -16,6 +16,7 @@ use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; @@ -83,6 +84,28 @@ public function testCommandDeleteFailed() $tester->execute(['pool' => 'foo', 'key' => 'bar']); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $application = new Application($this->getKernel()); + $application->add(new CachePoolDeleteCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); + $tester = new CommandCompletionTester($application->get('cache:pool:delete')); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'pool_name' => [ + ['f'], + ['foo'], + ]; + } + /** * @return MockObject&KernelInterface */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php new file mode 100644 index 0000000000000..a506ac2d2915f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/EventDispatcherDebugCommandTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\EventDispatcherDebugCommand; +use Symfony\Component\Console\Tester\CommandCompletionTester; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\EventDispatcher\EventDispatcher; + +class EventDispatcherDebugCommandTest extends TestCase +{ + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $tester = $this->createCommandCompletionTester(); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'event' => [[''], ['Symfony\Component\Mailer\Event\MessageEvent', 'console.command']]; + yield 'event for other dispatcher' => [['--dispatcher', 'other_event_dispatcher', ''], ['other_event', 'App\OtherEvent']]; + yield 'dispatcher' => [['--dispatcher='], ['event_dispatcher', 'other_event_dispatcher']]; + yield 'format' => [['--format='], ['txt', 'xml', 'json', 'md']]; + } + + private function createCommandCompletionTester(): CommandCompletionTester + { + $dispatchers = new ServiceLocator([ + 'event_dispatcher' => function () { + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('Symfony\Component\Mailer\Event\MessageEvent', 'var_dump'); + $dispatcher->addListener('console.command', 'var_dump'); + + return $dispatcher; + }, + 'other_event_dispatcher' => function () { + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('other_event', 'var_dump'); + $dispatcher->addListener('App\OtherEvent', 'var_dump'); + + return $dispatcher; + }, + ]); + $command = new EventDispatcherDebugCommand($dispatchers); + + return new CommandCompletionTester($command); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRemoveCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRemoveCommandTest.php new file mode 100644 index 0000000000000..213e639f06698 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsRemoveCommandTest.php @@ -0,0 +1,37 @@ +createMock(AbstractVault::class); + $vault->method('list')->willReturn(['SECRET' => null, 'OTHER_SECRET' => null]); + if ($withLocalVault) { + $localVault = $this->createMock(AbstractVault::class); + $localVault->method('list')->willReturn(['SECRET' => null]); + } else { + $localVault = null; + } + $command = new SecretsRemoveCommand($vault, $localVault); + $tester = new CommandCompletionTester($command); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'name' => [true, [''], ['SECRET', 'OTHER_SECRET']]; + yield '--local name (with local vault)' => [true, ['--local', ''], ['SECRET']]; + yield '--local name (without local vault)' => [false, ['--local', ''], []]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsSetCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsSetCommandTest.php new file mode 100644 index 0000000000000..4f0d2225d148a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/SecretsSetCommandTest.php @@ -0,0 +1,31 @@ +createMock(AbstractVault::class); + $vault->method('list')->willReturn(['SECRET' => null, 'OTHER_SECRET' => null]); + $localVault = $this->createMock(AbstractVault::class); + $command = new SecretsSetCommand($vault, $localVault); + $tester = new CommandCompletionTester($command); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'name' => [[''], ['SECRET', 'OTHER_SECRET']]; + yield '--local name (with local vault)' => [['--local', ''], ['SECRET', 'OTHER_SECRET']]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php index d013e1b40ba7d..70f94d6a34d48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php @@ -14,10 +14,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ExtensionWithoutConfigTestBundle\ExtensionWithoutConfigTestBundle; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpKernel; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Translation\Extractor\ExtractorInterface; @@ -32,59 +34,48 @@ class TranslationDebugCommandTest extends TestCase public function testDebugMissingMessages() { $tester = $this->createCommandTester(['foo' => 'foo']); - $tester->execute(['locale' => 'en', 'bundle' => 'foo']); + $res = $tester->execute(['locale' => 'en', 'bundle' => 'foo']); $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); + $this->assertSame(TranslationDebugCommand::EXIT_CODE_MISSING, $res); } public function testDebugUnusedMessages() { $tester = $this->createCommandTester([], ['foo' => 'foo']); - $tester->execute(['locale' => 'en', 'bundle' => 'foo']); + $res = $tester->execute(['locale' => 'en', 'bundle' => 'foo']); $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); + $this->assertSame(TranslationDebugCommand::EXIT_CODE_UNUSED, $res); } public function testDebugFallbackMessages() { - $tester = $this->createCommandTester([], ['foo' => 'foo']); - $tester->execute(['locale' => 'fr', 'bundle' => 'foo']); + $tester = $this->createCommandTester(['foo' => 'foo'], ['foo' => 'foo']); + $res = $tester->execute(['locale' => 'fr', 'bundle' => 'foo']); $this->assertMatchesRegularExpression('/fallback/', $tester->getDisplay()); + $this->assertSame(TranslationDebugCommand::EXIT_CODE_FALLBACK, $res); } public function testNoDefinedMessages() { $tester = $this->createCommandTester(); - $tester->execute(['locale' => 'fr', 'bundle' => 'test']); + $res = $tester->execute(['locale' => 'fr', 'bundle' => 'test']); $this->assertMatchesRegularExpression('/No defined or extracted messages for locale "fr"/', $tester->getDisplay()); + $this->assertSame(TranslationDebugCommand::EXIT_CODE_GENERAL_ERROR, $res); } public function testDebugDefaultDirectory() { $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar']); - $tester->execute(['locale' => 'en']); - - $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); - $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); - } - - /** - * @group legacy - * @expectedDeprecation Storing translations in the "%ssf_translation%s/Resources/translations" directory is deprecated since Symfony 4.2, use the "%ssf_translation%s/translations" directory instead. - * @expectedDeprecation Loading Twig templates from the "%ssf_translation%s/Resources/views" directory is deprecated since Symfony 4.2, use the "%ssf_translation%s/templates" directory instead. - */ - public function testDebugLegacyDefaultDirectory() - { - $this->fs->mkdir($this->translationDir.'/Resources/translations'); - $this->fs->mkdir($this->translationDir.'/Resources/views'); - - $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar']); - $tester->execute(['locale' => 'en']); + $res = $tester->execute(['locale' => 'en']); + $expectedExitStatus = TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED; $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); + $this->assertSame($expectedExitStatus, $res); } public function testDebugDefaultRootDirectory() @@ -95,11 +86,14 @@ public function testDebugDefaultRootDirectory() $this->fs->mkdir($this->translationDir.'/translations'); $this->fs->mkdir($this->translationDir.'/templates'); + $expectedExitStatus = TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED; + $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar'], null, [$this->translationDir.'/trans'], [$this->translationDir.'/views']); - $tester->execute(['locale' => 'en']); + $res = $tester->execute(['locale' => 'en']); $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); + $this->assertSame($expectedExitStatus, $res); } public function testDebugCustomDirectory() @@ -112,11 +106,14 @@ public function testDebugCustomDirectory() ->with($this->equalTo($this->translationDir.'/customDir')) ->willThrowException(new \InvalidArgumentException()); + $expectedExitStatus = TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED; + $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar'], $kernel); - $tester->execute(['locale' => 'en', 'bundle' => $this->translationDir.'/customDir']); + $res = $tester->execute(['locale' => 'en', 'bundle' => $this->translationDir.'/customDir']); $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); + $this->assertSame($expectedExitStatus, $res); } public function testDebugInvalidDirectory() @@ -132,6 +129,22 @@ public function testDebugInvalidDirectory() $tester->execute(['locale' => 'en', 'bundle' => 'dir']); } + public function testNoErrorWithOnlyMissingOptionAndNoResults() + { + $tester = $this->createCommandTester([], ['foo' => 'foo']); + $res = $tester->execute(['locale' => 'en', '--only-missing' => true]); + + $this->assertSame(Command::SUCCESS, $res); + } + + public function testNoErrorWithOnlyUnusedOptionAndNoResults() + { + $tester = $this->createCommandTester(['foo' => 'foo']); + $res = $tester->execute(['locale' => 'en', '--only-unused' => true]); + + $this->assertSame(Command::SUCCESS, $res); + } + protected function setUp(): void { $this->fs = new Filesystem(); @@ -145,7 +158,12 @@ protected function tearDown(): void $this->fs->remove($this->translationDir); } - private function createCommandTester($extractedMessages = [], $loadedMessages = [], $kernel = null, array $transPaths = [], array $viewsPaths = []): CommandTester + private function createCommandTester(array $extractedMessages = [], array $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandTester + { + return new CommandTester($this->createCommand($extractedMessages, $loadedMessages, $kernel, $transPaths, $codePaths)); + } + + private function createCommand(array $extractedMessages = [], array $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = [], ExtractorInterface $extractor = null, array $bundles = [], array $enabledLocales = []): TranslationDebugCommand { $translator = $this->createMock(Translator::class); $translator @@ -153,15 +171,17 @@ private function createCommandTester($extractedMessages = [], $loadedMessages = ->method('getFallbackLocales') ->willReturn(['en']); - $extractor = $this->createMock(ExtractorInterface::class); - $extractor - ->expects($this->any()) - ->method('extract') - ->willReturnCallback( - function ($path, $catalogue) use ($extractedMessages) { - $catalogue->add($extractedMessages); - } - ); + if (!$extractor) { + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessages) { + $catalogue->add($extractedMessages); + } + ); + } $loader = $this->createMock(TranslationReader::class); $loader @@ -178,12 +198,6 @@ function ($path, $catalogue) use ($loadedMessages) { ['foo', $this->getBundle($this->translationDir)], ['test', $this->getBundle('test')], ]; - if (HttpKernel\Kernel::VERSION_ID < 40000) { - $returnValues = [ - ['foo', true, $this->getBundle($this->translationDir)], - ['test', true, $this->getBundle('test')], - ]; - } $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) @@ -194,22 +208,20 @@ function ($path, $catalogue) use ($loadedMessages) { $kernel ->expects($this->any()) ->method('getBundles') - ->willReturn([]); + ->willReturn($bundles); $container = new Container(); - $container->setParameter('kernel.root_dir', $this->translationDir); - $kernel ->expects($this->any()) ->method('getContainer') ->willReturn($container); - $command = new TranslationDebugCommand($translator, $loader, $extractor, $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $viewsPaths); + $command = new TranslationDebugCommand($translator, $loader, $extractor, $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, $enabledLocales); $application = new Application($kernel); $application->add($command); - return new CommandTester($application->find('debug:translation')); + return $application->find('debug:translation'); } private function getBundle($path) @@ -223,4 +235,55 @@ private function getBundle($path) return $bundle; } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $extractedMessagesWithDomains = [ + 'messages' => [ + 'foo' => 'foo', + ], + 'validators' => [ + 'foo' => 'foo', + ], + 'custom_domain' => [ + 'foo' => 'foo', + ], + ]; + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessagesWithDomains) { + foreach ($extractedMessagesWithDomains as $domain => $message) { + $catalogue->add($message, $domain); + } + } + ); + + $tester = new CommandCompletionTester($this->createCommand([], [], null, [], [], $extractor, [new ExtensionWithoutConfigTestBundle()], ['fr', 'nl'])); + $suggestions = $tester->complete($input); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + yield 'locale' => [ + [''], + ['fr', 'nl'], + ]; + + yield 'bundle' => [ + ['fr', '--domain', 'messages', ''], + ['ExtensionWithoutConfigTestBundle', 'extension_without_config_test'], + ]; + + yield 'option --domain' => [ + ['en', '--domain', ''], + ['messages', 'validators', 'custom_domain'], + ]; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php new file mode 100644 index 0000000000000..49ba74caf6a1b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandCompletionTest.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\Writer\TranslationWriter; + +class TranslationUpdateCommandCompletionTest extends TestCase +{ + private $fs; + private $translationDir; + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $tester = $this->createCommandCompletionTester(['messages' => ['foo' => 'foo']]); + + $suggestions = $tester->complete($input); + + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions() + { + $bundle = new ExtensionPresentBundle(); + + yield 'locale' => [[''], ['en', 'fr']]; + yield 'bundle' => [['en', ''], [$bundle->getName(), $bundle->getContainerExtension()->getAlias()]]; + yield 'domain with locale' => [['en', '--domain=m'], ['messages']]; + yield 'domain without locale' => [['--domain=m'], []]; + yield 'format' => [['en', '--format='], ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res', 'xlf12', 'xlf20']]; + yield 'sort' => [['en', '--sort='], ['asc', 'desc']]; + } + + protected function setUp(): void + { + $this->fs = new Filesystem(); + $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true); + $this->fs->mkdir($this->translationDir.'/translations'); + $this->fs->mkdir($this->translationDir.'/templates'); + } + + protected function tearDown(): void + { + $this->fs->remove($this->translationDir); + } + + private function createCommandCompletionTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandCompletionTester + { + $translator = $this->createMock(Translator::class); + $translator + ->expects($this->any()) + ->method('getFallbackLocales') + ->willReturn(['en']); + + $extractor = $this->createMock(ExtractorInterface::class); + $extractor + ->expects($this->any()) + ->method('extract') + ->willReturnCallback( + function ($path, $catalogue) use ($extractedMessages) { + foreach ($extractedMessages as $domain => $messages) { + $catalogue->add($messages, $domain); + } + } + ); + + $loader = $this->createMock(TranslationReader::class); + $loader + ->expects($this->any()) + ->method('read') + ->willReturnCallback( + function ($path, $catalogue) use ($loadedMessages) { + $catalogue->add($loadedMessages); + } + ); + + $writer = $this->createMock(TranslationWriter::class); + $writer + ->expects($this->any()) + ->method('getFormats') + ->willReturn( + ['php', 'xlf', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'ini', 'json', 'res'] + ); + + if (null === $kernel) { + $returnValues = [ + ['foo', $this->getBundle($this->translationDir)], + ['test', $this->getBundle('test')], + ]; + $kernel = $this->createMock(KernelInterface::class); + $kernel + ->expects($this->any()) + ->method('getBundle') + ->willReturnMap($returnValues); + } + + $kernel + ->expects($this->any()) + ->method('getBundles') + ->willReturn([new ExtensionPresentBundle()]); + + $container = new Container(); + $kernel + ->expects($this->any()) + ->method('getContainer') + ->willReturn($container); + + $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']); + + $application = new Application($kernel); + $application->add($command); + + return new CommandCompletionTester($application->find('translation:extract')); + } + + private function getBundle($path) + { + $bundle = $this->createMock(BundleInterface::class); + $bundle + ->expects($this->any()) + ->method('getPath') + ->willReturn($path) + ; + + return $bundle; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php index cdb438889e554..5c6fa8ec35ea2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php @@ -17,7 +17,6 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpKernel; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Translation\Extractor\ExtractorInterface; @@ -30,7 +29,7 @@ class TranslationUpdateCommandTest extends TestCase private $fs; private $translationDir; - public function testDumpMessagesAndClean() + public function testDumpMessagesAndCleanWithDeprecatedCommandName() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); @@ -38,10 +37,26 @@ public function testDumpMessagesAndClean() $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } + public function testDumpMessagesAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); + $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); + } + + public function testDumpMessagesAsTreeAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--as-tree' => 1]); + $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); + } + public function testDumpSortedMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'asc']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'asc']); $this->assertMatchesRegularExpression("/\*bar\*foo\*test/", preg_replace('/\s+/', '', $tester->getDisplay())); $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); } @@ -49,7 +64,7 @@ public function testDumpSortedMessagesAndClean() public function testDumpReverseSortedMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'desc']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'desc']); $this->assertMatchesRegularExpression("/\*test\*foo\*bar/", preg_replace('/\s+/', '', $tester->getDisplay())); $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); } @@ -57,7 +72,7 @@ public function testDumpReverseSortedMessagesAndClean() public function testDumpSortWithoutValueAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort']); $this->assertMatchesRegularExpression("/\*bar\*foo\*test/", preg_replace('/\s+/', '', $tester->getDisplay())); $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); } @@ -65,7 +80,7 @@ public function testDumpSortWithoutValueAndClean() public function testDumpWrongSortAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'test']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'test']); $this->assertMatchesRegularExpression('/\[ERROR\] Wrong sort order/', $tester->getDisplay()); } @@ -77,7 +92,7 @@ public function testDumpMessagesAndCleanInRootDirectory() $this->fs->mkdir($this->translationDir.'/templates'); $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']], [], null, [$this->translationDir.'/trans'], [$this->translationDir.'/views']); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--dump-messages' => true, '--clean' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', '--dump-messages' => true, '--clean' => true]); $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } @@ -85,7 +100,7 @@ public function testDumpMessagesAndCleanInRootDirectory() public function testDumpTwoMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/bar/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/2 messages were successfully extracted/', $tester->getDisplay()); @@ -94,7 +109,7 @@ public function testDumpTwoMessagesAndClean() public function testDumpMessagesForSpecificDomain() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo'], 'mydomain' => ['bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--domain' => 'mydomain']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--domain' => 'mydomain']); $this->assertMatchesRegularExpression('/bar/', $tester->getDisplay()); $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } @@ -102,7 +117,7 @@ public function testDumpMessagesForSpecificDomain() public function testWriteMessages() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--force' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true]); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } @@ -114,31 +129,14 @@ public function testWriteMessagesInRootDirectory() $this->fs->mkdir($this->translationDir.'/templates'); $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--force' => true]); - $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); - } - - /** - * @group legacy - * @expectedDeprecation Storing translations in the "%ssf_translation%s/Resources/translations" directory is deprecated since Symfony 4.2, use the "%ssf_translation%s/translations" directory instead. - * @expectedDeprecation Storing templates in the "%ssf_translation%s/Resources/views" directory is deprecated since Symfony 4.2, use the "%ssf_translation%s/templates" directory instead. - */ - public function testWriteMessagesInLegacyRootDirectory() - { - $this->fs->remove($this->translationDir); - $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true); - $this->fs->mkdir($this->translationDir.'/Resources/translations'); - $this->fs->mkdir($this->translationDir.'/Resources/views'); - - $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--force' => true]); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', '--force' => true]); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } public function testWriteMessagesForSpecificDomain() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo'], 'mydomain' => ['bar' => 'bar']]); - $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--domain' => 'mydomain']); + $tester->execute(['command' => 'translation:extract', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--domain' => 'mydomain']); $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } @@ -155,10 +153,7 @@ protected function tearDown(): void $this->fs->remove($this->translationDir); } - /** - * @return CommandTester - */ - private function createCommandTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $viewsPaths = []) + private function createCommandTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $codePaths = []): CommandTester { $translator = $this->createMock(Translator::class); $translator @@ -201,12 +196,6 @@ function ($path, $catalogue) use ($loadedMessages) { ['foo', $this->getBundle($this->translationDir)], ['test', $this->getBundle('test')], ]; - if (HttpKernel\Kernel::VERSION_ID < 40000) { - $returnValues = [ - ['foo', true, $this->getBundle($this->translationDir)], - ['test', true, $this->getBundle('test')], - ]; - } $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) @@ -220,18 +209,17 @@ function ($path, $catalogue) use ($loadedMessages) { ->willReturn([]); $container = new Container(); - $container->setParameter('kernel.root_dir', $this->translationDir); $kernel ->expects($this->any()) ->method('getContainer') ->willReturn($container); - $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $viewsPaths); + $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths); $application = new Application($kernel); $application->add($command); - return new CommandTester($application->find('translation:update')); + return new CommandTester($application->find('translation:extract')); } private function getBundle($path) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/WorkflowDumpCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/WorkflowDumpCommandTest.php new file mode 100644 index 0000000000000..13a63b40d97fa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/WorkflowDumpCommandTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; + +class WorkflowDumpCommandTest extends TestCase +{ + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $application = new Application(); + $application->add(new WorkflowDumpCommand([])); + + $tester = new CommandCompletionTester($application->find('workflow:dump')); + $suggestions = $tester->complete($input, 2); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'option --dump-format' => [['--dump-format', ''], ['puml', 'mermaid', 'dot']]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php index 48af23514d8d9..2de30d44939e5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php @@ -51,7 +51,7 @@ public function testLintFilesFromBundleDirectory() ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); - $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('[OK] All 0 XLIFF files contain valid syntax', trim($tester->getDisplay())); } @@ -115,9 +115,9 @@ protected function tearDown(): void { foreach ($this->files as $file) { if (file_exists($file)) { - unlink($file); + @unlink($file); } } - rmdir(sys_get_temp_dir().'/xliff-lint-test'); + @rmdir(sys_get_temp_dir().'/xliff-lint-test'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php index 0644c45ddfbad..37d6d8f7fa888 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php @@ -40,7 +40,7 @@ public function testLintCorrectFile() ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); - $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('OK', trim($tester->getDisplay())); } @@ -88,7 +88,7 @@ public function testLintFilesFromBundleDirectory() ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); - $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('[OK] All 0 YAML files contain valid syntax', trim($tester->getDisplay())); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 447d2afb04125..d075ad75f32c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; @@ -109,29 +108,6 @@ public function testBundleCommandCanBeFoundByAlias() $this->assertSame($command, $application->find('alias')); } - /** - * @group legacy - * @expectedDeprecation The "Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand" class is deprecated since Symfony 4.2, use "Symfony\Component\Console\Command\Command" with dependency injection instead. - */ - public function testBundleCommandsHaveRightContainer() - { - $command = $this->getMockForAbstractClass(ContainerAwareCommand::class, ['foo'], '', true, true, true, ['setContainer']); - $command->setCode(function () {}); - $command->expects($this->exactly(2))->method('setContainer'); - - $application = new Application($this->getKernel([], true)); - $application->setAutoExit(false); - $application->setCatchExceptions(false); - $application->add($command); - $tester = new ApplicationTester($application); - - // set container is called here - $tester->run(['command' => 'foo']); - - // as the container might have change between two runs, setContainer must called again - $tester->run(['command' => 'foo']); - } - public function testBundleCommandCanOverriddeAPreExistingCommandWithTheSameName() { $command = new Command('example'); @@ -171,7 +147,7 @@ public function testRunOnlyWarnsOnUnregistrableCommand() $tester->run(['command' => 'fine']); $output = $tester->getDisplay(); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('Some commands could not be registered:', $output); $this->assertStringContainsString('throwing', $output); $this->assertStringContainsString('fine', $output); @@ -228,7 +204,7 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() $tester = new ApplicationTester($application); $tester->run(['command' => 'list']); - $this->assertSame(0, $tester->getStatusCode()); + $tester->assertCommandIsSuccessful(); $display = explode('List commands', $tester->getDisplay()); $this->assertStringContainsString(trim('[WARNING] Some commands could not be registered:'), trim($display[1])); @@ -236,7 +212,7 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() public function testSuggestingPackagesWithExactMatch() { - $result = $this->createEventForSuggestingPackages('server:dump', []); + $result = $this->createEventForSuggestingPackages('doctrine:fixtures', []); $this->assertMatchesRegularExpression('/You may be looking for a command provided by/', $result); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php index d45814da781e6..3a38da0d14ed6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -229,6 +229,19 @@ public function getClassDescriptionTestData() ]; } + /** + * @dataProvider getDeprecationsTestData + */ + public function testGetDeprecations(ContainerBuilder $builder, $expectedDescription) + { + $this->assertDescription($expectedDescription, $builder, ['deprecations' => true]); + } + + public function getDeprecationsTestData() + { + return $this->getDescriptionTestData(ObjectsProvider::getContainerDeprecations()); + } + abstract protected function getDescriptor(); abstract protected function getFormat(); @@ -320,7 +333,7 @@ public function getDescribeContainerBuilderWithPriorityTagsTestData(): array foreach ($variations as $suffix => $options) { $file = sprintf('%s_%s.%s', trim($name, '.'), $suffix, $this->getFormat()); $description = file_get_contents(__DIR__.'/../../Fixtures/Descriptor/'.$file); - $data[] = [$object, $description, $options, $file]; + $data[] = [$object, $description, $options]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php index 05031fa7fcfb6..a9fc9252bf350 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Suit; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -102,6 +103,24 @@ public static function getContainerParameter() ]; } + public static function getContainerDeprecations() + { + $builderWithDeprecations = new ContainerBuilder(); + $builderWithDeprecations->setParameter('kernel.cache_dir', __DIR__.'/../../Fixtures/Descriptor/cache'); + $builderWithDeprecations->setParameter('kernel.build_dir', __DIR__.'/../../Fixtures/Descriptor/cache'); + $builderWithDeprecations->setParameter('kernel.container_class', 'KernelContainerWith'); + + $builderWithoutDeprecations = new ContainerBuilder(); + $builderWithoutDeprecations->setParameter('kernel.cache_dir', __DIR__.'/../../Fixtures/Descriptor/cache'); + $builderWithoutDeprecations->setParameter('kernel.build_dir', __DIR__.'/../../Fixtures/Descriptor/cache'); + $builderWithoutDeprecations->setParameter('kernel.container_class', 'KernelContainerWithout'); + + return [ + 'deprecations' => $builderWithDeprecations, + 'deprecations_empty' => $builderWithoutDeprecations, + ]; + } + public static function getContainerBuilders() { $builder1 = new ContainerBuilder(); @@ -142,6 +161,7 @@ public static function getContainerDefinitions() new Reference('definition_1'), new Reference('.definition_2'), ])) + ->addArgument(new AbstractArgument('placeholder')) ->setFactory(['Full\\Qualified\\FactoryClass', 'get']), '.definition_2' => $definition2 ->setPublic(false) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php index ce4f377c508fd..b844a60e7789b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php @@ -12,16 +12,50 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor; use Symfony\Bundle\FrameworkBundle\Console\Descriptor\TextDescriptor; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\Routing\Route; class TextDescriptorTest extends AbstractDescriptorTest { + private $fileLinkFormatter = null; + protected function getDescriptor() { - return new TextDescriptor(); + return new TextDescriptor($this->fileLinkFormatter); } protected function getFormat() { return 'txt'; } + + public function getDescribeRouteWithControllerLinkTestData() + { + $getDescribeData = $this->getDescribeRouteTestData(); + + foreach ($getDescribeData as $key => &$data) { + $routeStub = $data[0]; + $routeStub->setDefault('_controller', sprintf('%s::%s', MyController::class, '__invoke')); + $file = $data[2]; + $file = preg_replace('#(\..*?)$#', '_link$1', $file); + $data = file_get_contents(__DIR__.'/../../Fixtures/Descriptor/'.$file); + $data = [$routeStub, $data, $file]; + } + + return $getDescribeData; + } + + /** @dataProvider getDescribeRouteWithControllerLinkTestData */ + public function testDescribeRouteWithControllerLink(Route $route, $expectedDescription) + { + $this->fileLinkFormatter = new FileLinkFormatter('myeditor://open?file=%f&line=%l'); + parent::testDescribeRoute($route, str_replace('[:file:]', __FILE__, $expectedDescription)); + } +} + +class MyController +{ + public function __invoke() + { + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php index ccaa415e15955..f4215d39832de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php @@ -11,14 +11,44 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; -use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormConfigInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\WebLink\Link; +use Twig\Environment; -class AbstractControllerTest extends ControllerTraitTest +class AbstractControllerTest extends TestCase { protected function createController() { @@ -36,15 +66,10 @@ public function testSubscribedServices() 'request_stack' => '?Symfony\\Component\\HttpFoundation\\RequestStack', 'http_kernel' => '?Symfony\\Component\\HttpKernel\\HttpKernelInterface', 'serializer' => '?Symfony\\Component\\Serializer\\SerializerInterface', - 'session' => '?Symfony\\Component\\HttpFoundation\\Session\\SessionInterface', 'security.authorization_checker' => '?Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface', - 'templating' => '?Symfony\\Component\\Templating\\EngineInterface', 'twig' => '?Twig\\Environment', - 'doctrine' => '?Doctrine\\Persistence\\ManagerRegistry', 'form.factory' => '?Symfony\\Component\\Form\\FormFactoryInterface', 'parameter_bag' => '?Symfony\\Component\\DependencyInjection\\ParameterBag\\ContainerBagInterface', - 'message_bus' => '?Symfony\\Component\\Messenger\\MessageBusInterface', - 'messenger.default_bus' => '?Symfony\\Component\\Messenger\\MessageBusInterface', 'security.token_storage' => '?Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorageInterface', 'security.csrf.token_manager' => '?Symfony\\Component\\Security\\Csrf\\CsrfTokenManagerInterface', ]; @@ -78,47 +103,499 @@ public function testMissingParameterBag() $controller->getParameter('foo'); } -} -class TestAbstractController extends AbstractController -{ - private $throwOnUnexpectedService; + public function testForward() + { + $request = Request::create('/'); + $request->setLocale('fr'); + $request->setRequestFormat('xml'); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $kernel = $this->createMock(HttpKernelInterface::class); + $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { + return new Response($request->getRequestFormat().'--'.$request->getLocale()); + }); + + $container = new Container(); + $container->set('request_stack', $requestStack); + $container->set('http_kernel', $kernel); + + $controller = $this->createController(); + $controller->setContainer($container); + + $response = $controller->forward('a_controller'); + $this->assertEquals('xml--fr', $response->getContent()); + } + + public function testGetUser() + { + $user = new InMemoryUser('user', 'pass'); + $token = new UsernamePasswordToken($user, 'default', ['ROLE_USER']); + + $controller = $this->createController(); + $controller->setContainer($this->getContainerWithTokenStorage($token)); + + $this->assertSame($controller->getUser(), $user); + } + + public function testGetUserWithEmptyTokenStorage() + { + $controller = $this->createController(); + $controller->setContainer($this->getContainerWithTokenStorage(null)); + + $this->assertNull($controller->getUser()); + } + + public function testGetUserWithEmptyContainer() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The SecurityBundle is not registered in your application.'); + + $controller = $this->createController(); + $controller->setContainer(new Container()); + + $controller->getUser(); + } + + private function getContainerWithTokenStorage($token = null): Container + { + $tokenStorage = $this->createMock(TokenStorage::class); + $tokenStorage + ->expects($this->once()) + ->method('getToken') + ->willReturn($token); - public function __construct($throwOnUnexpectedService = true) + $container = new Container(); + $container->set('security.token_storage', $tokenStorage); + + return $container; + } + + public function testJson() { - $this->throwOnUnexpectedService = $throwOnUnexpectedService; + $controller = $this->createController(); + $controller->setContainer(new Container()); + + $response = $controller->json([]); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('[]', $response->getContent()); } - public function __call(string $method, array $arguments) + public function testJsonWithSerializer() { - return $this->$method(...$arguments); + $container = new Container(); + + $serializer = $this->createMock(SerializerInterface::class); + $serializer + ->expects($this->once()) + ->method('serialize') + ->with([], 'json', ['json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS]) + ->willReturn('[]'); + + $container->set('serializer', $serializer); + + $controller = $this->createController(); + $controller->setContainer($container); + + $response = $controller->json([]); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('[]', $response->getContent()); } - public function setContainer(ContainerInterface $container): ?ContainerInterface + public function testJsonWithSerializerContextOverride() { - if (!$this->throwOnUnexpectedService) { - return parent::setContainer($container); + $container = new Container(); + + $serializer = $this->createMock(SerializerInterface::class); + $serializer + ->expects($this->once()) + ->method('serialize') + ->with([], 'json', ['json_encode_options' => 0, 'other' => 'context']) + ->willReturn('[]'); + + $container->set('serializer', $serializer); + + $controller = $this->createController(); + $controller->setContainer($container); + + $response = $controller->json([], 200, [], ['json_encode_options' => 0, 'other' => 'context']); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertEquals('[]', $response->getContent()); + $response->setEncodingOptions(\JSON_FORCE_OBJECT); + $this->assertEquals('{}', $response->getContent()); + } + + public function testFile() + { + $container = new Container(); + $kernel = $this->createMock(HttpKernelInterface::class); + $container->set('http_kernel', $kernel); + + $controller = $this->createController(); + $controller->setContainer($container); + + /* @var BinaryFileResponse $response */ + $response = $controller->file(new File(__FILE__)); + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + if ($response->headers->get('content-type')) { + $this->assertSame('text/x-php', $response->headers->get('content-type')); } + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); + } - $expected = self::getSubscribedServices(); - - foreach ($container->getServiceIds() as $id) { - if ('service_container' === $id) { - continue; - } - if (!isset($expected[$id])) { - throw new \UnexpectedValueException(sprintf('Service "%s" is not expected, as declared by "%s::getSubscribedServices()".', $id, AbstractController::class)); - } - $type = substr($expected[$id], 1); - if (!$container->get($id) instanceof $type) { - throw new \UnexpectedValueException(sprintf('Service "%s" is expected to be an instance of "%s", as declared by "%s::getSubscribedServices()".', $id, $type, AbstractController::class)); - } + public function testFileAsInline() + { + $controller = $this->createController(); + + /* @var BinaryFileResponse $response */ + $response = $controller->file(new File(__FILE__), null, ResponseHeaderBag::DISPOSITION_INLINE); + + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + if ($response->headers->get('content-type')) { + $this->assertSame('text/x-php', $response->headers->get('content-type')); + } + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); + $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); + } + + public function testFileWithOwnFileName() + { + $controller = $this->createController(); + + /* @var BinaryFileResponse $response */ + $fileName = 'test.php'; + $response = $controller->file(new File(__FILE__), $fileName); + + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + if ($response->headers->get('content-type')) { + $this->assertSame('text/x-php', $response->headers->get('content-type')); } + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString($fileName, $response->headers->get('content-disposition')); + } + + public function testFileWithOwnFileNameAsInline() + { + $controller = $this->createController(); + + /* @var BinaryFileResponse $response */ + $fileName = 'test.php'; + $response = $controller->file(new File(__FILE__), $fileName, ResponseHeaderBag::DISPOSITION_INLINE); - return parent::setContainer($container); + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + if ($response->headers->get('content-type')) { + $this->assertSame('text/x-php', $response->headers->get('content-type')); + } + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); + $this->assertStringContainsString($fileName, $response->headers->get('content-disposition')); } - public function fooAction() + public function testFileFromPath() { + $controller = $this->createController(); + + /* @var BinaryFileResponse $response */ + $response = $controller->file(__FILE__); + + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + if ($response->headers->get('content-type')) { + $this->assertSame('text/x-php', $response->headers->get('content-type')); + } + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); + } + + public function testFileFromPathWithCustomizedFileName() + { + $controller = $this->createController(); + + /* @var BinaryFileResponse $response */ + $response = $controller->file(__FILE__, 'test.php'); + + $this->assertInstanceOf(BinaryFileResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + if ($response->headers->get('content-type')) { + $this->assertSame('text/x-php', $response->headers->get('content-type')); + } + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString('test.php', $response->headers->get('content-disposition')); + } + + public function testFileWhichDoesNotExist() + { + $this->expectException(FileNotFoundException::class); + + $controller = $this->createController(); + + $controller->file('some-file.txt', 'test.php'); + } + + public function testIsGranted() + { + $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); + $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true); + + $container = new Container(); + $container->set('security.authorization_checker', $authorizationChecker); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertTrue($controller->isGranted('foo')); + } + + public function testdenyAccessUnlessGranted() + { + $this->expectException(AccessDeniedException::class); + + $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); + $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(false); + + $container = new Container(); + $container->set('security.authorization_checker', $authorizationChecker); + + $controller = $this->createController(); + $controller->setContainer($container); + + $controller->denyAccessUnlessGranted('foo'); + } + + public function testRenderViewTwig() + { + $twig = $this->createMock(Environment::class); + $twig->expects($this->once())->method('render')->willReturn('bar'); + + $container = new Container(); + $container->set('twig', $twig); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertEquals('bar', $controller->renderView('foo')); + } + + public function testRenderTwig() + { + $twig = $this->createMock(Environment::class); + $twig->expects($this->once())->method('render')->willReturn('bar'); + + $container = new Container(); + $container->set('twig', $twig); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertEquals('bar', $controller->render('foo')->getContent()); + } + + public function testRenderFormNew() + { + $formView = new FormView(); + + $form = $this->getMockBuilder(FormInterface::class)->getMock(); + $form->expects($this->once())->method('createView')->willReturn($formView); + + $twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(); + $twig->expects($this->once())->method('render')->with('foo', ['bar' => $formView])->willReturn('bar'); + + $container = new Container(); + $container->set('twig', $twig); + + $controller = $this->createController(); + $controller->setContainer($container); + + $response = $controller->renderForm('foo', ['bar' => $form]); + + $this->assertTrue($response->isSuccessful()); + $this->assertSame('bar', $response->getContent()); + } + + public function testRenderFormSubmittedAndInvalid() + { + $formView = new FormView(); + + $form = $this->getMockBuilder(FormInterface::class)->getMock(); + $form->expects($this->once())->method('createView')->willReturn($formView); + $form->expects($this->once())->method('isSubmitted')->willReturn(true); + $form->expects($this->once())->method('isValid')->willReturn(false); + + $twig = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->getMock(); + $twig->expects($this->once())->method('render')->with('foo', ['bar' => $formView])->willReturn('bar'); + + $container = new Container(); + $container->set('twig', $twig); + + $controller = $this->createController(); + $controller->setContainer($container); + + $response = $controller->renderForm('foo', ['bar' => $form]); + + $this->assertSame(422, $response->getStatusCode()); + $this->assertSame('bar', $response->getContent()); + } + + public function testStreamTwig() + { + $twig = $this->createMock(Environment::class); + + $container = new Container(); + $container->set('twig', $twig); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertInstanceOf(StreamedResponse::class, $controller->stream('foo')); + } + + public function testRedirectToRoute() + { + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once())->method('generate')->willReturn('/foo'); + + $container = new Container(); + $container->set('router', $router); + + $controller = $this->createController(); + $controller->setContainer($container); + $response = $controller->redirectToRoute('foo'); + + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertSame('/foo', $response->getTargetUrl()); + $this->assertSame(302, $response->getStatusCode()); + } + + /** + * @runInSeparateProcess + */ + public function testAddFlash() + { + $flashBag = new FlashBag(); + $session = $this->createMock(Session::class); + $session->expects($this->once())->method('getFlashBag')->willReturn($flashBag); + + $request = new Request(); + $request->setSession($session); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $container = new Container(); + $container->set('request_stack', $requestStack); + + $controller = $this->createController(); + $controller->setContainer($container); + $controller->addFlash('foo', 'bar'); + + $this->assertSame(['bar'], $flashBag->get('foo')); + } + + public function testCreateAccessDeniedException() + { + $controller = $this->createController(); + + $this->assertInstanceOf(AccessDeniedException::class, $controller->createAccessDeniedException()); + } + + public function testIsCsrfTokenValid() + { + $tokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); + + $container = new Container(); + $container->set('security.csrf.token_manager', $tokenManager); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertTrue($controller->isCsrfTokenValid('foo', 'bar')); + } + + public function testGenerateUrl() + { + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once())->method('generate')->willReturn('/foo'); + + $container = new Container(); + $container->set('router', $router); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertEquals('/foo', $controller->generateUrl('foo')); + } + + public function testRedirect() + { + $controller = $this->createController(); + $response = $controller->redirect('https://dunglas.fr', 301); + + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertSame('https://dunglas.fr', $response->getTargetUrl()); + $this->assertSame(301, $response->getStatusCode()); + } + + public function testCreateNotFoundException() + { + $controller = $this->createController(); + + $this->assertInstanceOf(NotFoundHttpException::class, $controller->createNotFoundException()); + } + + public function testCreateForm() + { + $config = $this->createMock(FormConfigInterface::class); + $config->method('getInheritData')->willReturn(false); + $config->method('getName')->willReturn(''); + + $form = new Form($config); + + $formFactory = $this->createMock(FormFactoryInterface::class); + $formFactory->expects($this->once())->method('create')->willReturn($form); + + $container = new Container(); + $container->set('form.factory', $formFactory); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertEquals($form, $controller->createForm('foo')); + } + + public function testCreateFormBuilder() + { + $formBuilder = $this->createMock(FormBuilderInterface::class); + + $formFactory = $this->createMock(FormFactoryInterface::class); + $formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); + + $container = new Container(); + $container->set('form.factory', $formFactory); + + $controller = $this->createController(); + $controller->setContainer($container); + + $this->assertEquals($formBuilder, $controller->createFormBuilder('foo')); + } + + public function testAddLink() + { + $request = new Request(); + $link1 = new Link('mercure', 'https://demo.mercure.rocks'); + $link2 = new Link('self', 'https://example.com/foo'); + + $controller = $this->createController(); + $controller->addLink($request, $link1); + $controller->addLink($request, $link2); + + $links = $request->attributes->get('_links')->getLinks(); + $this->assertContains($link1, $links); + $this->assertContains($link2, $links); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php deleted file mode 100644 index c3f0cd26a85e6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php +++ /dev/null @@ -1,191 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; - -use Composer\Autoload\ClassLoader; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\HttpKernel\Bundle\BundleInterface; -use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\HttpKernel\KernelInterface; - -/** - * @group legacy - */ -class ControllerNameParserTest extends TestCase -{ - protected $loader; - - protected function setUp(): void - { - $this->loader = new ClassLoader(); - $this->loader->add('TestBundle', __DIR__.'/../Fixtures'); - $this->loader->add('TestApplication', __DIR__.'/../Fixtures'); - $this->loader->register(); - } - - protected function tearDown(): void - { - $this->loader->unregister(); - $this->loader = null; - } - - public function testParse() - { - $parser = $this->createParser(); - - $this->assertEquals('TestBundle\FooBundle\Controller\DefaultController::indexAction', $parser->parse('FooBundle:Default:index'), '->parse() converts a short a:b:c notation string to a class::method string'); - $this->assertEquals('TestBundle\FooBundle\Controller\Sub\DefaultController::indexAction', $parser->parse('FooBundle:Sub\Default:index'), '->parse() converts a short a:b:c notation string to a class::method string'); - $this->assertEquals('TestBundle\Sensio\Cms\FooBundle\Controller\DefaultController::indexAction', $parser->parse('SensioCmsFooBundle:Default:index'), '->parse() converts a short a:b:c notation string to a class::method string'); - $this->assertEquals('TestBundle\FooBundle\Controller\Test\DefaultController::indexAction', $parser->parse('FooBundle:Test\\Default:index'), '->parse() converts a short a:b:c notation string to a class::method string'); - $this->assertEquals('TestBundle\FooBundle\Controller\Test\DefaultController::indexAction', $parser->parse('FooBundle:Test/Default:index'), '->parse() converts a short a:b:c notation string to a class::method string'); - - try { - $parser->parse('foo:'); - $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an a:b:c string'); - } catch (\Exception $e) { - $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an a:b:c string'); - } - } - - public function testBuild() - { - $parser = $this->createParser(); - - $this->assertEquals('FoooooBundle:Default:index', $parser->build('TestBundle\FooBundle\Controller\DefaultController::indexAction'), '->parse() converts a class::method string to a short a:b:c notation string'); - $this->assertEquals('FoooooBundle:Sub\Default:index', $parser->build('TestBundle\FooBundle\Controller\Sub\DefaultController::indexAction'), '->parse() converts a class::method string to a short a:b:c notation string'); - - try { - $parser->build('TestBundle\FooBundle\Controller\DefaultController::index'); - $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); - } catch (\Exception $e) { - $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); - } - - try { - $parser->build('TestBundle\FooBundle\Controller\Default::indexAction'); - $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); - } catch (\Exception $e) { - $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); - } - - try { - $parser->build('Foo\Controller\DefaultController::indexAction'); - $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); - } catch (\Exception $e) { - $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); - } - } - - /** - * @dataProvider getMissingControllersTest - */ - public function testMissingControllers($name) - { - $parser = $this->createParser(); - - try { - $parser->parse($name); - $this->fail('->parse() throws a \InvalidArgumentException if the class is found but does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws a \InvalidArgumentException if the class is found but does not exist'); - } - } - - public function getMissingControllersTest() - { - // a normal bundle - $bundles = [ - ['FooBundle:Fake:index'], - ]; - - // a bundle with children - if (Kernel::VERSION_ID < 40000) { - $bundles[] = ['SensioFooBundle:Fake:index']; - } - - return $bundles; - } - - /** - * @dataProvider getInvalidBundleNameTests - */ - public function testInvalidBundleName($bundleName, $suggestedBundleName) - { - $parser = $this->createParser(); - - try { - $parser->parse($bundleName); - $this->fail('->parse() throws a \InvalidArgumentException if the bundle does not exist'); - } catch (\Exception $e) { - $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws a \InvalidArgumentException if the bundle does not exist'); - - if (false === $suggestedBundleName) { - // make sure we don't have a suggestion - $this->assertStringNotContainsString('Did you mean', $e->getMessage()); - } else { - $this->assertStringContainsString(sprintf('Did you mean "%s"', $suggestedBundleName), $e->getMessage()); - } - } - } - - public function getInvalidBundleNameTests() - { - return [ - 'Alternative will be found using levenshtein' => ['FoodBundle:Default:index', 'FooBundle:Default:index'], - 'Bundle does not exist at all' => ['CrazyBundle:Default:index', false], - ]; - } - - private function createParser() - { - $bundles = [ - 'SensioCmsFooBundle' => $this->getBundle('TestBundle\Sensio\Cms\FooBundle', 'SensioCmsFooBundle'), - 'FooBundle' => $this->getBundle('TestBundle\FooBundle', 'FooBundle'), - ]; - - $kernel = $this->createMock(KernelInterface::class); - $kernel - ->expects($this->any()) - ->method('getBundle') - ->willReturnCallback(function ($bundle) use ($bundles) { - if (!isset($bundles[$bundle])) { - throw new \InvalidArgumentException(sprintf('Invalid bundle name "%s"', $bundle)); - } - - return $bundles[$bundle]; - }) - ; - - $bundles = [ - 'SensioCmsFooBundle' => $this->getBundle('TestBundle\Sensio\Cms\FooBundle', 'SensioCmsFooBundle'), - 'FoooooBundle' => $this->getBundle('TestBundle\FooBundle', 'FoooooBundle'), - 'FooBundle' => $this->getBundle('TestBundle\FooBundle', 'FooBundle'), - ]; - $kernel - ->expects($this->any()) - ->method('getBundles') - ->willReturn($bundles) - ; - - return new ControllerNameParser($kernel); - } - - private function getBundle($namespace, $name) - { - $bundle = $this->createMock(BundleInterface::class); - $bundle->expects($this->any())->method('getName')->willReturn($name); - $bundle->expects($this->any())->method('getNamespace')->willReturn($namespace); - - return $bundle; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index 83a84cddfeef9..5fb3e774a709d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -14,7 +14,6 @@ use Psr\Container\ContainerInterface as Psr11ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerAwareInterface; @@ -49,31 +48,6 @@ public function testGetControllerOnContainerAwareInvokable() $this->assertInstanceOf(ContainerInterface::class, $controller->getContainer()); } - /** - * @group legacy - * @expectedDeprecation Referencing controllers with FooBundle:Default:test is deprecated since Symfony 4.1. Use Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController::testAction instead. - */ - public function testGetControllerWithBundleNotation() - { - $shortName = 'FooBundle:Default:test'; - $parser = $this->createMockParser(); - $parser->expects($this->once()) - ->method('parse') - ->with($shortName) - ->willReturn('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController::testAction') - ; - - $resolver = $this->createLegacyControllerResolver(null, null, $parser); - $request = Request::create('/'); - $request->attributes->set('_controller', $shortName); - - $controller = $resolver->getController($request); - - $this->assertInstanceOf(ContainerAwareController::class, $controller[0]); - $this->assertInstanceOf(ContainerInterface::class, $controller[0]->getContainer()); - $this->assertSame('testAction', $controller[1]); - } - public function testContainerAwareControllerGetsContainerWhenNotSet() { class_exists(AbstractControllerTest::class); @@ -92,12 +66,11 @@ class_exists(AbstractControllerTest::class); $this->assertSame($container, $controller->getContainer()); } - /** - * @group legacy - * @expectedDeprecation Auto-injection of the container for "Symfony\Bundle\FrameworkBundle\Tests\Controller\TestAbstractController" is deprecated since Symfony 4.2. Configure it as a service instead. - */ public function testAbstractControllerGetsContainerWhenNotSet() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('"Symfony\\Bundle\\FrameworkBundle\\Tests\\Controller\\TestAbstractController" has no container set, did you forget to define it as a service subscriber?'); + class_exists(AbstractControllerTest::class); $controller = new TestAbstractController(false); @@ -105,7 +78,7 @@ class_exists(AbstractControllerTest::class); $container = new Container(); $container->set(TestAbstractController::class, $controller); - $resolver = $this->createLegacyControllerResolver(null, $container); + $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', TestAbstractController::class.'::fooAction'); @@ -114,12 +87,11 @@ class_exists(AbstractControllerTest::class); $this->assertSame($container, $controller->setContainer($container)); } - /** - * @group legacy - * @expectedDeprecation Auto-injection of the container for "Symfony\Bundle\FrameworkBundle\Tests\Controller\DummyController" is deprecated since Symfony 4.2. Configure it as a service instead. - */ - public function testAbstractControllerServiceWithFcqnIdGetsContainerWhenNotSet() + public function testAbstractControllerServiceWithFqcnIdGetsContainerWhenNotSet() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('"Symfony\\Bundle\\FrameworkBundle\\Tests\\Controller\\DummyController" has no container set, did you forget to define it as a service subscriber?'); + class_exists(AbstractControllerTest::class); $controller = new DummyController(); @@ -127,7 +99,7 @@ class_exists(AbstractControllerTest::class); $container = new Container(); $container->set(DummyController::class, $controller); - $resolver = $this->createLegacyControllerResolver(null, $container); + $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', DummyController::class.'::fooAction'); @@ -176,19 +148,6 @@ class_exists(AbstractControllerTest::class); $this->assertSame($controllerContainer, $controller->getContainer()); } - protected function createLegacyControllerResolver(LoggerInterface $logger = null, Psr11ContainerInterface $container = null, ControllerNameParser $parser = null) - { - if (!$parser) { - $parser = $this->createMockParser(); - } - - if (!$container) { - $container = $this->createMockContainer(); - } - - return new ControllerResolver($container, $parser, $logger); - } - protected function createControllerResolver(LoggerInterface $logger = null, Psr11ContainerInterface $container = null) { if (!$container) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php deleted file mode 100644 index feff96bc44448..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTest.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; - -/** - * @group legacy - */ -class ControllerTest extends ControllerTraitTest -{ - protected function createController() - { - return new TestController(); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php deleted file mode 100644 index fbeb68d6df8da..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ /dev/null @@ -1,567 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; - -use Doctrine\Persistence\ManagerRegistry; -use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormConfigInterface; -use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\HttpFoundation\BinaryFileResponse; -use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; -use Symfony\Component\HttpFoundation\File\File; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; -use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\StreamedResponse; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\Routing\RouterInterface; -use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; -use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\WebLink\Link; -use Twig\Environment; - -abstract class ControllerTraitTest extends TestCase -{ - abstract protected function createController(); - - public function testForward() - { - $request = Request::create('/'); - $request->setLocale('fr'); - $request->setRequestFormat('xml'); - - $requestStack = new RequestStack(); - $requestStack->push($request); - - $kernel = $this->createMock(HttpKernelInterface::class); - $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { - return new Response($request->getRequestFormat().'--'.$request->getLocale()); - }); - - $container = new Container(); - $container->set('request_stack', $requestStack); - $container->set('http_kernel', $kernel); - - $controller = $this->createController(); - $controller->setContainer($container); - - $response = $controller->forward('a_controller'); - $this->assertEquals('xml--fr', $response->getContent()); - } - - public function testGetUser() - { - $user = new User('user', 'pass'); - $token = new UsernamePasswordToken($user, 'pass', 'default', ['ROLE_USER']); - - $controller = $this->createController(); - $controller->setContainer($this->getContainerWithTokenStorage($token)); - - $this->assertSame($controller->getUser(), $user); - } - - public function testGetUserAnonymousUserConvertedToNull() - { - $token = new AnonymousToken('default', 'anon.'); - - $controller = $this->createController(); - $controller->setContainer($this->getContainerWithTokenStorage($token)); - - $this->assertNull($controller->getUser()); - } - - public function testGetUserWithEmptyTokenStorage() - { - $controller = $this->createController(); - $controller->setContainer($this->getContainerWithTokenStorage(null)); - - $this->assertNull($controller->getUser()); - } - - public function testGetUserWithEmptyContainer() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The SecurityBundle is not registered in your application.'); - $controller = $this->createController(); - $controller->setContainer(new Container()); - - $controller->getUser(); - } - - private function getContainerWithTokenStorage($token = null): Container - { - $tokenStorage = $this->createMock(TokenStorage::class); - $tokenStorage - ->expects($this->once()) - ->method('getToken') - ->willReturn($token); - - $container = new Container(); - $container->set('security.token_storage', $tokenStorage); - - return $container; - } - - public function testJson() - { - $controller = $this->createController(); - $controller->setContainer(new Container()); - - $response = $controller->json([]); - $this->assertInstanceOf(JsonResponse::class, $response); - $this->assertEquals('[]', $response->getContent()); - } - - public function testJsonWithSerializer() - { - $container = new Container(); - - $serializer = $this->createMock(SerializerInterface::class); - $serializer - ->expects($this->once()) - ->method('serialize') - ->with([], 'json', ['json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS]) - ->willReturn('[]'); - - $container->set('serializer', $serializer); - - $controller = $this->createController(); - $controller->setContainer($container); - - $response = $controller->json([]); - $this->assertInstanceOf(JsonResponse::class, $response); - $this->assertEquals('[]', $response->getContent()); - } - - public function testJsonWithSerializerContextOverride() - { - $container = new Container(); - - $serializer = $this->createMock(SerializerInterface::class); - $serializer - ->expects($this->once()) - ->method('serialize') - ->with([], 'json', ['json_encode_options' => 0, 'other' => 'context']) - ->willReturn('[]'); - - $container->set('serializer', $serializer); - - $controller = $this->createController(); - $controller->setContainer($container); - - $response = $controller->json([], 200, [], ['json_encode_options' => 0, 'other' => 'context']); - $this->assertInstanceOf(JsonResponse::class, $response); - $this->assertEquals('[]', $response->getContent()); - $response->setEncodingOptions(\JSON_FORCE_OBJECT); - $this->assertEquals('{}', $response->getContent()); - } - - public function testFile() - { - $container = new Container(); - $kernel = $this->createMock(HttpKernelInterface::class); - $container->set('http_kernel', $kernel); - - $controller = $this->createController(); - $controller->setContainer($container); - - /* @var BinaryFileResponse $response */ - $response = $controller->file(new File(__FILE__)); - $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertSame(200, $response->getStatusCode()); - if ($response->headers->get('content-type')) { - $this->assertSame('text/x-php', $response->headers->get('content-type')); - } - $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); - } - - public function testFileAsInline() - { - $controller = $this->createController(); - - /* @var BinaryFileResponse $response */ - $response = $controller->file(new File(__FILE__), null, ResponseHeaderBag::DISPOSITION_INLINE); - - $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertSame(200, $response->getStatusCode()); - if ($response->headers->get('content-type')) { - $this->assertSame('text/x-php', $response->headers->get('content-type')); - } - $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); - $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); - } - - public function testFileWithOwnFileName() - { - $controller = $this->createController(); - - /* @var BinaryFileResponse $response */ - $fileName = 'test.php'; - $response = $controller->file(new File(__FILE__), $fileName); - - $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertSame(200, $response->getStatusCode()); - if ($response->headers->get('content-type')) { - $this->assertSame('text/x-php', $response->headers->get('content-type')); - } - $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertStringContainsString($fileName, $response->headers->get('content-disposition')); - } - - public function testFileWithOwnFileNameAsInline() - { - $controller = $this->createController(); - - /* @var BinaryFileResponse $response */ - $fileName = 'test.php'; - $response = $controller->file(new File(__FILE__), $fileName, ResponseHeaderBag::DISPOSITION_INLINE); - - $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertSame(200, $response->getStatusCode()); - if ($response->headers->get('content-type')) { - $this->assertSame('text/x-php', $response->headers->get('content-type')); - } - $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); - $this->assertStringContainsString($fileName, $response->headers->get('content-disposition')); - } - - public function testFileFromPath() - { - $controller = $this->createController(); - - /* @var BinaryFileResponse $response */ - $response = $controller->file(__FILE__); - - $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertSame(200, $response->getStatusCode()); - if ($response->headers->get('content-type')) { - $this->assertSame('text/x-php', $response->headers->get('content-type')); - } - $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); - } - - public function testFileFromPathWithCustomizedFileName() - { - $controller = $this->createController(); - - /* @var BinaryFileResponse $response */ - $response = $controller->file(__FILE__, 'test.php'); - - $this->assertInstanceOf(BinaryFileResponse::class, $response); - $this->assertSame(200, $response->getStatusCode()); - if ($response->headers->get('content-type')) { - $this->assertSame('text/x-php', $response->headers->get('content-type')); - } - $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertStringContainsString('test.php', $response->headers->get('content-disposition')); - } - - public function testFileWhichDoesNotExist() - { - $this->expectException(FileNotFoundException::class); - $controller = $this->createController(); - - $controller->file('some-file.txt', 'test.php'); - } - - public function testIsGranted() - { - $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); - $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true); - - $container = new Container(); - $container->set('security.authorization_checker', $authorizationChecker); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertTrue($controller->isGranted('foo')); - } - - public function testdenyAccessUnlessGranted() - { - $this->expectException(AccessDeniedException::class); - $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); - $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(false); - - $container = new Container(); - $container->set('security.authorization_checker', $authorizationChecker); - - $controller = $this->createController(); - $controller->setContainer($container); - - $controller->denyAccessUnlessGranted('foo'); - } - - public function testRenderViewTwig() - { - $twig = $this->createMock(Environment::class); - $twig->expects($this->once())->method('render')->willReturn('bar'); - - $container = new Container(); - $container->set('twig', $twig); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals('bar', $controller->renderView('foo')); - } - - public function testRenderTwig() - { - $twig = $this->createMock(Environment::class); - $twig->expects($this->once())->method('render')->willReturn('bar'); - - $container = new Container(); - $container->set('twig', $twig); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals('bar', $controller->render('foo')->getContent()); - } - - public function testStreamTwig() - { - $twig = $this->createMock(Environment::class); - - $container = new Container(); - $container->set('twig', $twig); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertInstanceOf(StreamedResponse::class, $controller->stream('foo')); - } - - public function testRedirectToRoute() - { - $router = $this->createMock(RouterInterface::class); - $router->expects($this->once())->method('generate')->willReturn('/foo'); - - $container = new Container(); - $container->set('router', $router); - - $controller = $this->createController(); - $controller->setContainer($container); - $response = $controller->redirectToRoute('foo'); - - $this->assertInstanceOf(RedirectResponse::class, $response); - $this->assertSame('/foo', $response->getTargetUrl()); - $this->assertSame(302, $response->getStatusCode()); - } - - /** - * @runInSeparateProcess - */ - public function testAddFlash() - { - $flashBag = new FlashBag(); - $session = $this->createMock(Session::class); - $session->expects($this->once())->method('getFlashBag')->willReturn($flashBag); - - $container = new Container(); - $container->set('session', $session); - - $controller = $this->createController(); - $controller->setContainer($container); - $controller->addFlash('foo', 'bar'); - - $this->assertSame(['bar'], $flashBag->get('foo')); - } - - public function testCreateAccessDeniedException() - { - $controller = $this->createController(); - - $this->assertInstanceOf(AccessDeniedException::class, $controller->createAccessDeniedException()); - } - - public function testIsCsrfTokenValid() - { - $tokenManager = $this->createMock(CsrfTokenManagerInterface::class); - $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); - - $container = new Container(); - $container->set('security.csrf.token_manager', $tokenManager); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertTrue($controller->isCsrfTokenValid('foo', 'bar')); - } - - public function testGenerateUrl() - { - $router = $this->createMock(RouterInterface::class); - $router->expects($this->once())->method('generate')->willReturn('/foo'); - - $container = new Container(); - $container->set('router', $router); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals('/foo', $controller->generateUrl('foo')); - } - - public function testRedirect() - { - $controller = $this->createController(); - $response = $controller->redirect('https://dunglas.fr', 301); - - $this->assertInstanceOf(RedirectResponse::class, $response); - $this->assertSame('https://dunglas.fr', $response->getTargetUrl()); - $this->assertSame(301, $response->getStatusCode()); - } - - /** - * @group legacy - */ - public function testRenderViewTemplating() - { - $templating = $this->createMock(EngineInterface::class); - $templating->expects($this->once())->method('render')->willReturn('bar'); - $templating->expects($this->once())->method('supports')->willReturn(true); - - $container = new Container(); - $container->set('templating', $templating); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals('bar', $controller->renderView('foo')); - } - - /** - * @group legacy - */ - public function testRenderTemplating() - { - $templating = $this->createMock(EngineInterface::class); - $templating->expects($this->once())->method('render')->willReturn('bar'); - $templating->expects($this->once())->method('supports')->willReturn(true); - - $container = new Container(); - $container->set('templating', $templating); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals('bar', $controller->render('foo')->getContent()); - } - - /** - * @group legacy - */ - public function testStreamTemplating() - { - $templating = $this->createMock(EngineInterface::class); - - $container = new Container(); - $container->set('templating', $templating); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertInstanceOf(StreamedResponse::class, $controller->stream('foo')); - } - - public function testCreateNotFoundException() - { - $controller = $this->createController(); - - $this->assertInstanceOf(NotFoundHttpException::class, $controller->createNotFoundException()); - } - - public function testCreateForm() - { - $config = $this->createMock(FormConfigInterface::class); - $config->method('getInheritData')->willReturn(false); - $config->method('getName')->willReturn(''); - - $form = new Form($config); - - $formFactory = $this->createMock(FormFactoryInterface::class); - $formFactory->expects($this->once())->method('create')->willReturn($form); - - $container = new Container(); - $container->set('form.factory', $formFactory); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals($form, $controller->createForm('foo')); - } - - public function testCreateFormBuilder() - { - $formBuilder = $this->createMock(FormBuilderInterface::class); - - $formFactory = $this->createMock(FormFactoryInterface::class); - $formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); - - $container = new Container(); - $container->set('form.factory', $formFactory); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals($formBuilder, $controller->createFormBuilder('foo')); - } - - public function testGetDoctrine() - { - $doctrine = $this->createMock(ManagerRegistry::class); - - $container = new Container(); - $container->set('doctrine', $doctrine); - - $controller = $this->createController(); - $controller->setContainer($container); - - $this->assertEquals($doctrine, $controller->getDoctrine()); - } - - public function testAddLink() - { - $request = new Request(); - $link1 = new Link('mercure', 'https://demo.mercure.rocks'); - $link2 = new Link('self', 'https://example.com/foo'); - - $controller = $this->createController(); - $controller->addLink($request, $link1); - $controller->addLink($request, $link2); - - $links = $request->attributes->get('_links')->getLinks(); - $this->assertContains($link1, $links); - $this->assertContains($link2, $links); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php index 4230797b07d36..e567342ecf4c9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php @@ -12,9 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; -use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Twig\Environment; +use Twig\Loader\ArrayLoader; /** * @author Kévin Dunglas @@ -32,27 +32,46 @@ public function testTwig() $this->assertEquals('bar', $controller('mytemplate')->getContent()); } - /** - * @group legacy - */ - public function testTemplating() + public function testNoTwig() { - $templating = $this->createMock(EngineInterface::class); - $templating->expects($this->exactly(2))->method('render')->willReturn('bar'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('You cannot use the TemplateController if the Twig Bundle is not available.'); + $controller = new TemplateController(); + + $controller->templateAction('mytemplate')->getContent(); + $controller('mytemplate')->getContent(); + } - $controller = new TemplateController(null, $templating); + public function testContext() + { + $templateName = 'template_controller.html.twig'; + $context = [ + 'param' => 'hello world', + ]; + $expected = '

'.$context['param'].'

'; - $this->assertEquals('bar', $controller->templateAction('mytemplate')->getContent()); - $this->assertEquals('bar', $controller('mytemplate')->getContent()); + $loader = new ArrayLoader(); + $loader->setTemplate($templateName, '

{{param}}

'); + + $twig = new Environment($loader); + $controller = new TemplateController($twig); + + $this->assertEquals($expected, $controller->templateAction($templateName, null, null, null, $context)->getContent()); + $this->assertEquals($expected, $controller($templateName, null, null, null, $context)->getContent()); } - public function testNoTwigNorTemplating() + public function testStatusCode() { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('You can not use the TemplateController if the Templating Component or the Twig Bundle are not available.'); - $controller = new TemplateController(); + $templateName = 'template_controller.html.twig'; + $statusCode = 201; - $controller->templateAction('mytemplate')->getContent(); - $controller('mytemplate')->getContent(); + $loader = new ArrayLoader(); + $loader->setTemplate($templateName, '

{{param}}

'); + + $twig = new Environment($loader); + $controller = new TemplateController($twig); + + $this->assertSame(201, $controller->templateAction($templateName, null, null, null, [], $statusCode)->getStatusCode()); + $this->assertSame(200, $controller->templateAction($templateName)->getStatusCode()); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestAbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestAbstractController.php new file mode 100644 index 0000000000000..5238aee8ff07b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestAbstractController.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class TestAbstractController extends AbstractController +{ + private $throwOnUnexpectedService; + + public function __construct($throwOnUnexpectedService = true) + { + $this->throwOnUnexpectedService = $throwOnUnexpectedService; + } + + public function __call(string $method, array $arguments) + { + return $this->$method(...$arguments); + } + + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + if (!$this->throwOnUnexpectedService) { + return parent::setContainer($container); + } + + $expected = self::getSubscribedServices(); + + foreach ($container->getServiceIds() as $id) { + if ('service_container' === $id) { + continue; + } + if (!isset($expected[$id])) { + throw new \UnexpectedValueException(sprintf('Service "%s" is not expected, as declared by "%s::getSubscribedServices()".', $id, AbstractController::class)); + } + $type = substr($expected[$id], 1); + if (!$container->get($id) instanceof $type) { + throw new \UnexpectedValueException(sprintf('Service "%s" is expected to be an instance of "%s", as declared by "%s::getSubscribedServices()".', $id, $type, AbstractController::class)); + } + } + + return parent::setContainer($container); + } + + public function fooAction() + { + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php deleted file mode 100644 index 58a49797caa0b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php +++ /dev/null @@ -1,32 +0,0 @@ -addCompilerPass(new AddExpressionLanguageProvidersPass(false)); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); $definition = new Definition('\stdClass'); $definition->addTag('routing.expression_language_provider'); $container->setDefinition('some_routing_provider', $definition->setPublic(true)); - $container->register('router', '\stdClass')->setPublic(true); + $container->register('router.default', '\stdClass')->setPublic(true); $container->compile(); - $router = $container->getDefinition('router'); + $router = $container->getDefinition('router.default'); $calls = $router->getMethodCalls(); $this->assertCount(1, $calls); $this->assertEquals('addExpressionLanguageProvider', $calls[0][0]); @@ -41,14 +41,14 @@ public function testProcessForRouter() public function testProcessForRouterAlias() { $container = new ContainerBuilder(); - $container->addCompilerPass(new AddExpressionLanguageProvidersPass(false)); + $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); $definition = new Definition('\stdClass'); $definition->addTag('routing.expression_language_provider'); $container->setDefinition('some_routing_provider', $definition->setPublic(true)); $container->register('my_router', '\stdClass')->setPublic(true); - $container->setAlias('router', 'my_router'); + $container->setAlias('router.default', 'my_router'); $container->compile(); $router = $container->getDefinition('my_router'); @@ -57,88 +57,4 @@ public function testProcessForRouterAlias() $this->assertEquals('addExpressionLanguageProvider', $calls[0][0]); $this->assertEquals(new Reference('some_routing_provider'), $calls[0][1][0]); } - - /** - * @group legacy - * @expectedDeprecation Registering services tagged "security.expression_language_provider" with "Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass" is deprecated since Symfony 4.2, use the "Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass" instead. - */ - public function testProcessForSecurity() - { - $container = new ContainerBuilder(); - $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); - - $definition = new Definition('\stdClass'); - $definition->addTag('security.expression_language_provider'); - $container->setDefinition('some_security_provider', $definition->setPublic(true)); - - $container->register('security.expression_language', '\stdClass')->setPublic(true); - $container->compile(); - - $calls = $container->getDefinition('security.expression_language')->getMethodCalls(); - $this->assertCount(1, $calls); - $this->assertEquals('registerProvider', $calls[0][0]); - $this->assertEquals(new Reference('some_security_provider'), $calls[0][1][0]); - } - - /** - * @group legacy - * @expectedDeprecation Registering services tagged "security.expression_language_provider" with "Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass" is deprecated since Symfony 4.2, use the "Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass" instead. - */ - public function testProcessForSecurityAlias() - { - $container = new ContainerBuilder(); - $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); - - $definition = new Definition('\stdClass'); - $definition->addTag('security.expression_language_provider'); - $container->setDefinition('some_security_provider', $definition->setPublic(true)); - - $container->register('my_security.expression_language', '\stdClass')->setPublic(true); - $container->setAlias('security.expression_language', 'my_security.expression_language'); - $container->compile(); - - $calls = $container->getDefinition('my_security.expression_language')->getMethodCalls(); - $this->assertCount(1, $calls); - $this->assertEquals('registerProvider', $calls[0][0]); - $this->assertEquals(new Reference('some_security_provider'), $calls[0][1][0]); - } - - /** - * @group legacy - */ - public function testProcessIgnoreSecurity() - { - $container = new ContainerBuilder(); - $container->addCompilerPass(new AddExpressionLanguageProvidersPass(false)); - - $definition = new Definition('\stdClass'); - $definition->addTag('security.expression_language_provider'); - $container->setDefinition('some_security_provider', $definition->setPublic(true)); - - $container->register('security.expression_language', '\stdClass')->setPublic(true); - $container->compile(); - - $calls = $container->getDefinition('security.expression_language')->getMethodCalls(); - $this->assertCount(0, $calls); - } - - /** - * @group legacy - */ - public function testProcessIgnoreSecurityAlias() - { - $container = new ContainerBuilder(); - $container->addCompilerPass(new AddExpressionLanguageProvidersPass(false)); - - $definition = new Definition('\stdClass'); - $definition->addTag('security.expression_language_provider'); - $container->setDefinition('some_security_provider', $definition->setPublic(true)); - - $container->register('my_security.expression_language', '\stdClass')->setPublic(true); - $container->setAlias('security.expression_language', 'my_security.expression_language'); - $container->compile(); - - $calls = $container->getDefinition('my_security.expression_language')->getMethodCalls(); - $this->assertCount(0, $calls); - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CacheCollectorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CacheCollectorPassTest.php deleted file mode 100644 index d7613d5ae5d0a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CacheCollectorPassTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CacheCollectorPass; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Cache\Adapter\TagAwareAdapter; -use Symfony\Component\Cache\Adapter\TraceableAdapter; -use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter; -use Symfony\Component\Cache\DataCollector\CacheDataCollector; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @group legacy - */ -class CacheCollectorPassTest extends TestCase -{ - public function testProcess() - { - $container = new ContainerBuilder(); - $container - ->register('fs', FilesystemAdapter::class) - ->addTag('cache.pool'); - $container - ->register('tagged_fs', TagAwareAdapter::class) - ->addArgument(new Reference('fs')) - ->addTag('cache.pool'); - - $collector = $container->register('data_collector.cache', CacheDataCollector::class); - (new CacheCollectorPass())->process($container); - - $this->assertEquals([ - ['addInstance', ['fs', new Reference('fs')]], - ['addInstance', ['tagged_fs', new Reference('tagged_fs')]], - ], $collector->getMethodCalls()); - - $this->assertSame(TraceableAdapter::class, $container->findDefinition('fs')->getClass()); - $this->assertSame(TraceableTagAwareAdapter::class, $container->getDefinition('tagged_fs')->getClass()); - $this->assertFalse($collector->isPublic(), 'The "data_collector.cache" should be private after processing'); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php deleted file mode 100644 index e538461ea5994..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolClearerPass; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass; -use Symfony\Component\DependencyInjection\Compiler\RemoveUnusedDefinitionsPass; -use Symfony\Component\DependencyInjection\Compiler\RepeatedPass; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; - -/** - * @group legacy - */ -class CachePoolClearerPassTest extends TestCase -{ - public function testPoolRefsAreWeak() - { - $container = new ContainerBuilder(); - $container->setParameter('kernel.container_class', 'app'); - $container->setParameter('kernel.project_dir', 'foo'); - - $globalClearer = new Definition(Psr6CacheClearer::class); - $globalClearer->setPublic(true); - $container->setDefinition('cache.global_clearer', $globalClearer); - - $publicPool = new Definition(); - $publicPool->setPublic(true); - $publicPool->addArgument('namespace'); - $publicPool->addTag('cache.pool', ['clearer' => 'clearer_alias']); - $container->setDefinition('public.pool', $publicPool); - - $publicPool = new Definition(); - $publicPool->setPublic(true); - $publicPool->addArgument('namespace'); - $publicPool->addTag('cache.pool', ['clearer' => 'clearer_alias', 'name' => 'pool2']); - $container->setDefinition('public.pool2', $publicPool); - - $privatePool = new Definition(); - $privatePool->setPublic(false); - $privatePool->addArgument('namespace'); - $privatePool->addTag('cache.pool', ['clearer' => 'clearer_alias']); - $container->setDefinition('private.pool', $privatePool); - - $clearer = new Definition(); - $clearer->setPublic(true); - $container->setDefinition('clearer', $clearer); - $container->setAlias('clearer_alias', 'clearer'); - - $pass = new RemoveUnusedDefinitionsPass(); - foreach ($container->getCompiler()->getPassConfig()->getRemovingPasses() as $removingPass) { - if ($removingPass instanceof RepeatedPass) { - $pass->setRepeatedPass(new RepeatedPass([$pass])); - break; - } - } - foreach ([new CachePoolPass(), $pass, new CachePoolClearerPass()] as $pass) { - $pass->process($container); - } - - $expected = [[ - 'public.pool' => new Reference('public.pool'), - 'pool2' => new Reference('public.pool2'), - ]]; - $this->assertEquals($expected, $clearer->getArguments()); - $this->assertEquals($expected, $globalClearer->getArguments()); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php deleted file mode 100644 index 55a92488d5f2f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php +++ /dev/null @@ -1,131 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass; -use Symfony\Component\Cache\Adapter\ArrayAdapter; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @group legacy - */ -class CachePoolPassTest extends TestCase -{ - private $cachePoolPass; - - protected function setUp(): void - { - $this->cachePoolPass = new CachePoolPass(); - } - - public function testNamespaceArgumentIsReplaced() - { - $container = new ContainerBuilder(); - $container->setParameter('kernel.container_class', 'app'); - $container->setParameter('kernel.project_dir', 'foo'); - $adapter = new Definition(); - $adapter->setAbstract(true); - $adapter->addTag('cache.pool'); - $container->setDefinition('app.cache_adapter', $adapter); - $container->setAlias('app.cache_adapter_alias', 'app.cache_adapter'); - $cachePool = new ChildDefinition('app.cache_adapter_alias'); - $cachePool->addArgument(null); - $cachePool->addTag('cache.pool'); - $container->setDefinition('app.cache_pool', $cachePool); - - $this->cachePoolPass->process($container); - - $this->assertSame('z3X945Jbf5', $cachePool->getArgument(0)); - } - - public function testNamespaceArgumentIsNotReplacedIfArrayAdapterIsUsed() - { - $container = new ContainerBuilder(); - $container->setParameter('kernel.container_class', 'app'); - $container->setParameter('kernel.project_dir', 'foo'); - - $container->register('cache.adapter.array', ArrayAdapter::class)->addArgument(0); - - $cachePool = new ChildDefinition('cache.adapter.array'); - $cachePool->addTag('cache.pool'); - $container->setDefinition('app.cache_pool', $cachePool); - - $this->cachePoolPass->process($container); - - $this->assertCount(0, $container->getDefinition('app.cache_pool')->getArguments()); - } - - public function testArgsAreReplaced() - { - $container = new ContainerBuilder(); - $container->setParameter('kernel.container_class', 'app'); - $container->setParameter('cache.prefix.seed', 'foo'); - $cachePool = new Definition(); - $cachePool->addTag('cache.pool', [ - 'provider' => 'foobar', - 'default_lifetime' => 3, - ]); - $cachePool->addArgument(null); - $cachePool->addArgument(null); - $cachePool->addArgument(null); - $container->setDefinition('app.cache_pool', $cachePool); - - $this->cachePoolPass->process($container); - - $this->assertInstanceOf(Reference::class, $cachePool->getArgument(0)); - $this->assertSame('foobar', (string) $cachePool->getArgument(0)); - $this->assertSame('tQNhcV-8xa', $cachePool->getArgument(1)); - $this->assertSame(3, $cachePool->getArgument(2)); - } - - public function testWithNameAttribute() - { - $container = new ContainerBuilder(); - $container->setParameter('kernel.container_class', 'app'); - $container->setParameter('cache.prefix.seed', 'foo'); - $cachePool = new Definition(); - $cachePool->addTag('cache.pool', [ - 'name' => 'foobar', - 'provider' => 'foobar', - ]); - $cachePool->addArgument(null); - $cachePool->addArgument(null); - $cachePool->addArgument(null); - $container->setDefinition('app.cache_pool', $cachePool); - - $this->cachePoolPass->process($container); - - $this->assertSame('+naTpPa4Sm', $cachePool->getArgument(1)); - } - - public function testThrowsExceptionWhenCachePoolTagHasUnknownAttributes() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid "cache.pool" tag for service "app.cache_pool": accepted attributes are'); - $container = new ContainerBuilder(); - $container->setParameter('kernel.container_class', 'app'); - $container->setParameter('kernel.project_dir', 'foo'); - $adapter = new Definition(); - $adapter->setAbstract(true); - $adapter->addTag('cache.pool'); - $container->setDefinition('app.cache_adapter', $adapter); - $cachePool = new ChildDefinition('app.cache_adapter'); - $cachePool->addTag('cache.pool', ['foobar' => 123]); - $container->setDefinition('app.cache_pool', $cachePool); - - $this->cachePoolPass->process($container); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php deleted file mode 100644 index c2494d9cb2ec8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPrunerPass; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Cache\Adapter\PhpFilesAdapter; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @group legacy - */ -class CachePoolPrunerPassTest extends TestCase -{ - public function testCompilerPassReplacesCommandArgument() - { - $container = new ContainerBuilder(); - $container->register('console.command.cache_pool_prune')->addArgument([]); - $container->register('pool.foo', FilesystemAdapter::class)->addTag('cache.pool'); - $container->register('pool.bar', PhpFilesAdapter::class)->addTag('cache.pool'); - - $pass = new CachePoolPrunerPass(); - $pass->process($container); - - $expected = [ - 'pool.foo' => new Reference('pool.foo'), - 'pool.bar' => new Reference('pool.bar'), - ]; - $argument = $container->getDefinition('console.command.cache_pool_prune')->getArgument(0); - - $this->assertInstanceOf(IteratorArgument::class, $argument); - $this->assertEquals($expected, $argument->getValues()); - } - - public function testCompilePassIsIgnoredIfCommandDoesNotExist() - { - $container = new ContainerBuilder(); - - $definitionsBefore = \count($container->getDefinitions()); - $aliasesBefore = \count($container->getAliases()); - - $pass = new CachePoolPrunerPass(); - $pass->process($container); - - // the container is untouched (i.e. no new definitions or aliases) - $this->assertCount($definitionsBefore, $container->getDefinitions()); - $this->assertCount($aliasesBefore, $container->getAliases()); - } - - public function testCompilerPassThrowsOnInvalidDefinitionClass() - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Class "Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\NotFound" used for service "pool.not-found" cannot be found.'); - $container = new ContainerBuilder(); - $container->register('console.command.cache_pool_prune')->addArgument([]); - $container->register('pool.not-found', NotFound::class)->addTag('cache.pool'); - - $pass = new CachePoolPrunerPass(); - $pass->process($container); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php index 138a0e4bbc27a..0167f55101b7b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php @@ -108,7 +108,12 @@ public function getNotImplementingTranslatorBagInterfaceTranslatorClassNames() class TranslatorWithTranslatorBag implements TranslatorInterface { - public function trans($id, array $parameters = [], $domain = null, $locale = null): string + public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string { } + + public function getLocale(): string + { + return 'en'; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php index 17ffbd2347c49..65f047426ae44 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php @@ -12,8 +12,15 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; +use Symfony\Bundle\FrameworkBundle\DataCollector\TemplateAwareDataCollectorInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; class ProfilerPassTest extends TestCase { @@ -54,4 +61,64 @@ public function testValidCollector() $this->assertCount(1, $methodCalls); $this->assertEquals('add', $methodCalls[0][0]); // grab the method part of the first call } + + public function provideValidCollectorWithTemplateUsingAutoconfigure(): \Generator + { + yield [new class() implements TemplateAwareDataCollectorInterface { + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + } + + public function getName(): string + { + return static::class; + } + + public function reset() + { + } + + public static function getTemplate(): string + { + return 'foo'; + } + }]; + + yield [new class() extends AbstractDataCollector { + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + } + + public static function getTemplate(): string + { + return 'foo'; + } + }]; + } + + /** + * @dataProvider provideValidCollectorWithTemplateUsingAutoconfigure + */ + public function testValidCollectorWithTemplateUsingAutoconfigure(TemplateAwareDataCollectorInterface $dataCollector) + { + $container = new ContainerBuilder(); + $profilerDefinition = $container->register('profiler', 'ProfilerClass'); + + $container->registerForAutoconfiguration(DataCollectorInterface::class)->addTag('data_collector'); + $container->register('mydatacollector', \get_class($dataCollector))->setAutoconfigured(true); + + (new ResolveInstanceofConditionalsPass())->process($container); + (new ProfilerPass())->process($container); + + $idForTemplate = \get_class($dataCollector); + $this->assertSame(['mydatacollector' => [$idForTemplate, 'foo']], $container->getParameter('data_collector.templates')); + + // grab the method calls off of the "profiler" definition + $methodCalls = $profilerDefinition->getMethodCalls(); + $this->assertCount(1, $methodCalls); + $this->assertEquals('add', $methodCalls[0][0]); // grab the method part of the first call + + (new ResolveChildDefinitionsPass())->process($container); + $this->assertSame($idForTemplate, $container->get('mydatacollector')->getName()); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SessionPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SessionPassTest.php deleted file mode 100644 index afc6f9b4b2577..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SessionPassTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SessionPass; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -class SessionPassTest extends TestCase -{ - public function testProcess() - { - $arguments = [ - new Reference('session.flash_bag'), - new Reference('session.attribute_bag'), - ]; - $container = new ContainerBuilder(); - $container - ->register('session') - ->setArguments($arguments); - $container - ->register('session.flash_bag') - ->setFactory([new Reference('session'), 'getFlashBag']); - $container - ->register('session.attribute_bag') - ->setFactory([new Reference('session'), 'getAttributeBag']); - - (new SessionPass())->process($container); - - $this->assertSame($arguments, $container->getDefinition('session')->getArguments()); - $this->assertNull($container->getDefinition('session.flash_bag')->getFactory()); - $this->assertNull($container->getDefinition('session.attribute_bag')->getFactory()); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php index 70dbe1412ca9f..7dc9e6f59ec99 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php @@ -36,6 +36,12 @@ public function testProcess() ->setPublic(true) ->addArgument(new Reference('Test\private_used_shared_service')) ->addArgument(new Reference('Test\private_used_non_shared_service')) + ->addArgument(new Reference('Test\soon_private_service')) + ; + + $container->register('Test\soon_private_service') + ->setPublic(true) + ->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.42']) ; $container->register('Test\private_used_shared_service'); @@ -48,11 +54,13 @@ public function testProcess() $expected = [ 'Test\private_used_shared_service' => new ServiceClosureArgument(new Reference('Test\private_used_shared_service')), 'Test\private_used_non_shared_service' => new ServiceClosureArgument(new Reference('Test\private_used_non_shared_service')), - 'Psr\Container\ContainerInterface' => new ServiceClosureArgument(new Reference('service_container')), - 'Symfony\Component\DependencyInjection\ContainerInterface' => new ServiceClosureArgument(new Reference('service_container')), + 'Test\soon_private_service' => new ServiceClosureArgument(new Reference('.container.private.Test\soon_private_service')), ]; - $this->assertEquals($expected, $container->getDefinition('test.private_services_locator')->getArgument(0)); - $this->assertSame($container, $container->get('test.private_services_locator')->get('Psr\Container\ContainerInterface')); + + $privateServices = $container->getDefinition('test.private_services_locator')->getArgument(0); + unset($privateServices['Symfony\Component\DependencyInjection\ContainerInterface'], $privateServices['Psr\Container\ContainerInterface']); + + $this->assertEquals($expected, $privateServices); $this->assertFalse($container->getDefinition('Test\private_used_non_shared_service')->isShared()); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php index 89a35285ba234..433b798d81a94 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php @@ -43,13 +43,13 @@ public function testMissingKnownTags() private function getKnownTags(): array { - // get tags in UnusedTagsPass - $target = \dirname(__DIR__, 3).'/DependencyInjection/Compiler/UnusedTagsPass.php'; - $contents = file_get_contents($target); - preg_match('{private \$knownTags = \[(.+?)\];}sm', $contents, $matches); - $tags = array_values(array_filter(array_map(function ($str) { - return trim(preg_replace('{^ +\'(.+)\',}', '$1', $str)); - }, explode("\n", $matches[1])))); + $tags = \Closure::bind( + static function () { + return UnusedTagsPass::KNOWN_TAGS; + }, + null, + UnusedTagsPass::class + )(); sort($tags); return $tags; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php index 67c97263ccdfd..5bbd93722e2ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php @@ -37,6 +37,22 @@ public static function getDefinedTags(): array } } + // get all tags used in PHP configs + $files = Finder::create()->files()->name('*.php')->path('Resources')->notPath('Tests')->in(\dirname(__DIR__, 5)); + foreach ($files as $file) { + $contents = file_get_contents($file); + if (preg_match_all("{->tag\('([^']+)'}", $contents, $matches)) { + foreach ($matches[1] as $match) { + $tags[$match] = true; + } + } + if (preg_match_all("{tagged_(?:locator|iterator)\('([^']+)'}", $contents, $matches)) { + foreach ($matches[1] as $match) { + $tags[$match] = true; + } + } + } + // get all tags used in findTaggedServiceIds calls() $files = Finder::create()->files()->name('*.php')->path('DependencyInjection')->notPath('Tests')->in(\dirname(__DIR__, 5)); foreach ($files as $file) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index e4d36c522fbf2..aaae64bac01d4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -15,12 +15,17 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; use Symfony\Bundle\FullStack; +use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Notifier\Notifier; +use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter; +use Symfony\Component\Uid\Factory\UuidFactory; class ConfigurationTest extends TestCase { @@ -29,26 +34,7 @@ public function testDefaultConfig() $processor = new Processor(); $config = $processor->processConfiguration(new Configuration(true), [['secret' => 's3cr3t']]); - $this->assertEquals( - array_merge(['secret' => 's3cr3t', 'trusted_hosts' => []], self::getBundleDefaultConfig()), - $config - ); - } - - /** - * @group legacy - */ - public function testDoNoDuplicateDefaultFormResources() - { - $input = ['templating' => [ - 'form' => ['resources' => ['FrameworkBundle:Form']], - 'engines' => ['php'], - ]]; - - $processor = new Processor(); - $config = $processor->processConfiguration(new Configuration(true), [$input]); - - $this->assertEquals(['FrameworkBundle:Form'], $config['templating']['form']['resources']); + $this->assertEquals(self::getBundleDefaultConfig(), $config); } public function getTestValidSessionName() @@ -101,6 +87,7 @@ public function testAssetsCanBeEnabled() 'base_urls' => [], 'packages' => [], 'json_manifest_path' => null, + 'strict_mode' => false, ]; $this->assertEquals($defaultConfig, $config['assets']); @@ -384,6 +371,16 @@ protected static function getBundleDefaultConfig() 'http_method_override' => true, 'ide' => null, 'default_locale' => 'en', + 'enabled_locales' => [], + 'set_locale_from_accept_language' => false, + 'set_content_language_from_locale' => false, + 'secret' => 's3cr3t', + 'trusted_hosts' => [], + 'trusted_headers' => [ + 'x-forwarded-for', + 'x-forwarded-port', + 'x-forwarded-proto', + ], 'csrf_protection' => [ 'enabled' => false, ], @@ -404,9 +401,10 @@ protected static function getBundleDefaultConfig() 'profiler' => [ 'enabled' => false, 'only_exceptions' => false, - 'only_master_requests' => false, + 'only_main_requests' => false, 'dsn' => 'file:%kernel.cache_dir%/profiler', 'collect' => true, + 'collect_parameter' => null, ], 'translator' => [ 'enabled' => !class_exists(FullStack::class), @@ -416,6 +414,15 @@ protected static function getBundleDefaultConfig() 'formatter' => 'translator.formatter.default', 'paths' => [], 'default_path' => '%kernel.project_dir%/translations', + 'pseudo_localization' => [ + 'enabled' => false, + 'accents' => true, + 'expansion_factor' => 1.0, + 'brackets' => true, + 'parse_html' => false, + 'localizable_html_attributes' => [], + ], + 'providers' => [], ], 'validation' => [ 'enabled' => !class_exists(FullStack::class), @@ -438,12 +445,16 @@ protected static function getBundleDefaultConfig() 'enabled' => true, ], 'serializer' => [ + 'default_context' => [], 'enabled' => !class_exists(FullStack::class), 'enable_annotations' => !class_exists(FullStack::class), 'mapping' => ['paths' => []], ], 'property_access' => [ + 'enabled' => true, 'magic_call' => false, + 'magic_get' => true, + 'magic_set' => true, 'throw_exception_on_invalid_index' => false, 'throw_exception_on_invalid_property_path' => true, ], @@ -452,14 +463,15 @@ protected static function getBundleDefaultConfig() ], 'router' => [ 'enabled' => false, + 'default_uri' => null, 'http_port' => 80, 'https_port' => 443, 'strict_requirements' => true, - 'utf8' => false, + 'utf8' => true, ], 'session' => [ 'enabled' => false, - 'storage_id' => 'session.storage.native', + 'storage_factory_id' => 'session.storage.factory.native', 'handler_id' => 'session.handler.native_file', 'cookie_httponly' => true, 'cookie_samesite' => null, @@ -471,15 +483,6 @@ protected static function getBundleDefaultConfig() 'enabled' => false, 'formats' => [], ], - 'templating' => [ - 'enabled' => false, - 'hinclude_default_template' => null, - 'form' => [ - 'resources' => ['FrameworkBundle:Form'], - ], - 'engines' => [], - 'loaders' => [], - ], 'assets' => [ 'enabled' => !class_exists(FullStack::class), 'version_strategy' => null, @@ -489,15 +492,18 @@ protected static function getBundleDefaultConfig() 'base_urls' => [], 'packages' => [], 'json_manifest_path' => null, + 'strict_mode' => false, ], 'cache' => [ 'pools' => [], 'app' => 'cache.adapter.filesystem', 'system' => 'cache.adapter.system', - 'directory' => '%kernel.cache_dir%/pools', + 'directory' => '%kernel.cache_dir%/pools/app', 'default_redis_provider' => 'redis://localhost', 'default_memcached_provider' => 'memcached://localhost', - 'default_pdo_provider' => class_exists(Connection::class) ? 'database_connection' : null, + 'default_doctrine_dbal_provider' => 'database_connection', + 'default_pdo_provider' => ContainerBuilder::willBeAvailable('doctrine/dbal', Connection::class, ['symfony/framework-bundle']) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null, + 'prefix_seed' => '_%kernel.project_dir%.%kernel.container_class%', ], 'workflows' => [ 'enabled' => false, @@ -532,6 +538,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor ], 'default_bus' => null, 'buses' => ['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]], + 'reset_on_message' => true, ], 'disallow_search_engine_index' => true, 'http_client' => [ @@ -542,14 +549,40 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'dsn' => null, 'transports' => [], 'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class), + 'message_bus' => null, + 'headers' => [], + ], + 'notifier' => [ + 'enabled' => !class_exists(FullStack::class) && class_exists(Notifier::class), + 'chatter_transports' => [], + 'texter_transports' => [], + 'channel_policy' => [], + 'admin_recipients' => [], + 'notification_on_failed_messages' => false, ], 'error_controller' => 'error_controller', 'secrets' => [ 'enabled' => true, - 'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.environment%', + 'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.runtime_environment%', 'local_dotenv_file' => '%kernel.project_dir%/.env.%kernel.environment%.local', 'decryption_env_var' => 'base64:default::SYMFONY_DECRYPTION_SECRET', ], + 'http_cache' => [ + 'enabled' => false, + 'debug' => '%kernel.debug%', + 'private_headers' => [], + ], + 'rate_limiter' => [ + 'enabled' => !class_exists(FullStack::class) && class_exists(TokenBucketLimiter::class), + 'limiters' => [], + ], + 'uid' => [ + 'enabled' => !class_exists(FullStack::class) && class_exists(UuidFactory::class), + 'default_uuid_version' => 6, + 'name_based_uuid_version' => 5, + 'time_based_uuid_version' => 6, + ], + 'exceptions' => [], ]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php index c05c6fe3a1c86..f26621001c9ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/assets.php @@ -27,6 +27,22 @@ 'json_manifest_strategy' => [ 'json_manifest_path' => '/path/to/manifest.json', ], + 'remote_manifest' => [ + 'json_manifest_path' => 'https://cdn.example.com/manifest.json', + ], + 'var_manifest' => [ + 'json_manifest_path' => '%var_json_manifest_path%', + ], + 'env_manifest' => [ + 'json_manifest_path' => '%env(env_manifest)%', + ], + 'strict_manifest_strategy' => [ + 'json_manifest_path' => '/path/to/manifest.json', + 'strict_mode' => true, + ], ], ], ]); + +$container->setParameter('var_json_manifest_path', 'https://cdn.example.com/manifest.json'); +$container->setParameter('env(env_manifest)', 'https://cdn.example.com/manifest.json'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php index 5e607dfdbbfca..9ca04b6c63bf9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php @@ -7,11 +7,6 @@ 'adapter' => 'cache.adapter.apcu', 'default_lifetime' => 30, ], - 'cache.bar' => [ - 'adapter' => 'cache.adapter.doctrine', - 'default_lifetime' => 5, - 'provider' => 'app.doctrine_cache_provider', - ], 'cache.baz' => [ 'adapter' => 'cache.adapter.filesystem', 'default_lifetime' => 7, @@ -22,7 +17,10 @@ 'provider' => 'app.cache_pool', ], 'cache.def' => [ - 'default_lifetime' => 11, + 'default_lifetime' => 'PT11S', + ], + 'cache.expr' => [ + 'default_lifetime' => '13 seconds', ], 'cache.chain' => [ 'default_lifetime' => 12, @@ -37,6 +35,27 @@ 'default_lifetime' => 410, 'tags' => true, ], + 'cache.redis_tag_aware.foo' => [ + 'adapter' => 'cache.adapter.redis_tag_aware', + ], + 'cache.redis_tag_aware.foo2' => [ + 'tags' => true, + 'adapter' => 'cache.adapter.redis_tag_aware', + ], + 'cache.redis_tag_aware.bar' => [ + 'adapter' => 'cache.redis_tag_aware.foo', + ], + 'cache.redis_tag_aware.bar2' => [ + 'tags' => true, + 'adapter' => 'cache.redis_tag_aware.foo', + ], + 'cache.redis_tag_aware.baz' => [ + 'adapter' => 'cache.redis_tag_aware.foo2', + ], + 'cache.redis_tag_aware.baz2' => [ + 'tags' => true, + 'adapter' => 'cache.redis_tag_aware.foo2', + ], ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_app_redis_tag_aware.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_app_redis_tag_aware.php new file mode 100644 index 0000000000000..44855c62adbf1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_app_redis_tag_aware.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', [ + 'cache' => [ + 'app' => 'cache.adapter.redis_tag_aware', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_app_redis_tag_aware_pool.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_app_redis_tag_aware_pool.php new file mode 100644 index 0000000000000..bf3ee2de2b357 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_app_redis_tag_aware_pool.php @@ -0,0 +1,12 @@ +loadFromExtension('framework', [ + 'cache' => [ + 'app' => 'cache.redis_tag_aware.foo', + 'pools' => [ + 'cache.redis_tag_aware.foo' => [ + 'adapter' => 'cache.adapter.redis_tag_aware', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php index 886cb657b2dc6..8b712475fda58 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/csrf.php @@ -2,8 +2,8 @@ $container->loadFromExtension('framework', [ 'csrf_protection' => true, - 'form' => true, 'session' => [ + 'storage_factory_id' => 'session.storage.factory.native', 'handler_id' => null, ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php new file mode 100644 index 0000000000000..5d0dde0e0ac64 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php @@ -0,0 +1,12 @@ +loadFromExtension('framework', [ + 'exceptions' => [ + BadRequestHttpException::class => [ + 'log_level' => 'info', + 'status_code' => 422, + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_default_csrf.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_default_csrf.php new file mode 100644 index 0000000000000..a57d5233806fa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_default_csrf.php @@ -0,0 +1,11 @@ +loadFromExtension('framework', [ + 'form' => [ + 'legacy_error_messages' => false, + ], + 'session' => [ + 'storage_factory_id' => 'session.storage.factory.native', + 'handler_id' => null, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index e633d34187cf9..52903cd0b12a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'secret' => 's3cr3t', 'default_locale' => 'fr', + 'enabled_locales' => ['fr', 'en'], 'csrf_protection' => true, 'form' => [ 'csrf_protection' => [ @@ -23,9 +24,10 @@ 'router' => [ 'resource' => '%kernel.project_dir%/config/routing.xml', 'type' => 'xml', + 'utf8' => true, ], 'session' => [ - 'storage_id' => 'session.storage.native', + 'storage_factory_id' => 'session.storage.factory.native', 'handler_id' => 'session.handler.native_file', 'name' => '_SYMFONY', 'cookie_lifetime' => 86400, @@ -64,6 +66,7 @@ 'name_converter' => 'serializer.name_converter.camel_case_to_snake_case', 'circular_reference_handler' => 'my.circular.reference.handler', 'max_depth_handler' => 'my.max.depth.handler', + 'default_context' => ['enable_max_depth' => true], ], 'property_info' => true, 'ide' => 'file%%link%%format', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_mock_response_factory.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_mock_response_factory.php new file mode 100644 index 0000000000000..5b64c3ae0a1d4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_mock_response_factory.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => null, + 'mock_response_factory' => 'my_response_factory', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php new file mode 100644 index 0000000000000..f2ab01d1e1196 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_retry.php @@ -0,0 +1,23 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => [ + 'retry_failed' => [ + 'retry_strategy' => null, + 'http_codes' => [429, 500 => ['GET', 'HEAD']], + 'max_retries' => 2, + 'delay' => 100, + 'multiplier' => 2, + 'max_delay' => 0, + 'jitter' => 0.3, + ], + ], + 'scoped_clients' => [ + 'foo' => [ + 'base_uri' => 'http://example.com', + 'retry_failed' => ['multiplier' => 4], + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_translator_enabled_locales.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_translator_enabled_locales.php new file mode 100644 index 0000000000000..a585c6ee5de6d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/legacy_translator_enabled_locales.php @@ -0,0 +1,18 @@ +loadFromExtension('framework', [ + 'secret' => 's3cr3t', + 'default_locale' => 'fr', + 'router' => [ + 'resource' => '%kernel.project_dir%/config/routing.xml', + 'type' => 'xml', + 'utf8' => true, + ], + 'translator' => [ + 'enabled' => true, + 'fallback' => 'fr', + 'paths' => ['%kernel.project_dir%/Fixtures/translations'], + 'cache_dir' => '%kernel.cache_dir%/translations', + 'enabled_locales' => ['fr', 'en'], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php index ef8cdd385cf80..5e3093b33b431 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer.php @@ -7,5 +7,10 @@ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], ], + 'headers' => [ + 'from' => 'from@example.org', + 'bcc' => ['bcc1@example.org', 'bcc2@example.org'], + 'foo' => 'bar', + ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_disabled_message_bus.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_disabled_message_bus.php new file mode 100644 index 0000000000000..4f2471ed95802 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_disabled_message_bus.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'mailer' => [ + 'dsn' => 'smtp://example.com', + 'message_bus' => false, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php index 7eec06a9a0e50..df2ca46e46ee9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php @@ -10,6 +10,11 @@ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], ], + 'headers' => [ + 'from' => 'from@example.org', + 'bcc' => ['bcc1@example.org', 'bcc2@example.org'], + 'foo' => 'bar', + ], ], ]); }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_specific_message_bus.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_specific_message_bus.php new file mode 100644 index 0000000000000..32b936af9d88e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_specific_message_bus.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'mailer' => [ + 'dsn' => 'smtp://example.com', + 'message_bus' => 'app.another_bus', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php index 1bc79f3dd204c..8b13bc269b24a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php @@ -13,6 +13,11 @@ 'sender' => 'sender@example.org', 'recipients' => ['redirected@example.org', 'redirected1@example.org'], ], + 'headers' => [ + 'from' => 'from@example.org', + 'bcc' => ['bcc1@example.org', 'bcc2@example.org'], + 'foo' => 'bar', + ], ], ]); }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php new file mode 100644 index 0000000000000..8f85259aa6908 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php @@ -0,0 +1,19 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'transport_1' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_1' + ], + 'transport_2' => 'null://', + 'transport_3' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_3' + ], + 'failure_transport_1' => 'null://', + 'failure_transport_3' => 'null://' + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php new file mode 100644 index 0000000000000..0cff76887b152 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php @@ -0,0 +1,21 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'failure_transport' => 'failure_transport_global', + 'transports' => [ + 'transport_1' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_1' + ], + 'transport_2' => 'null://', + 'transport_3' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_3' + ], + 'failure_transport_global' => 'null://', + 'failure_transport_1' => 'null://', + 'failure_transport_3' => 'null://', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php index 0aff440e855e9..90c5def3ac100 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php @@ -22,6 +22,7 @@ ], 'failed' => 'in-memory:///', 'redis' => 'redis://127.0.0.1:6379/messages', + 'beanstalkd' => 'beanstalkd://127.0.0.1:11300', ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_disabled_reset_on_message.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_disabled_reset_on_message.php new file mode 100644 index 0000000000000..dda2e30108b87 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_disabled_reset_on_message.php @@ -0,0 +1,19 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'reset_on_message' => false, + 'routing' => [ + FooMessage::class => ['sender.bar', 'sender.biz'], + BarMessage::class => 'sender.foo', + ], + 'transports' => [ + 'sender.biz' => 'null://', + 'sender.bar' => 'null://', + 'sender.foo' => 'null://', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_explict_reset_on_message_legacy.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_explict_reset_on_message_legacy.php new file mode 100644 index 0000000000000..73102d522db57 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_with_explict_reset_on_message_legacy.php @@ -0,0 +1,19 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'reset_on_message' => true, + 'routing' => [ + FooMessage::class => ['sender.bar', 'sender.biz'], + BarMessage::class => 'sender.foo', + ], + 'transports' => [ + 'sender.biz' => 'null://', + 'sender.bar' => 'null://', + 'sender.foo' => 'null://', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php new file mode 100644 index 0000000000000..5ffe142be4dfc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier.php @@ -0,0 +1,30 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'enabled' => true + ], + 'mailer' => [ + 'dsn' => 'smtp://example.com', + ], + 'notifier' => [ + 'enabled' => true, + 'notification_on_failed_messages' => true, + 'chatter_transports' => [ + 'slack' => 'null' + ], + 'texter_transports' => [ + 'twilio' => 'null' + ], + 'channel_policy' => [ + 'low' => ['slack'], + 'high' => ['slack', 'twilio'], + ], + 'admin_recipients' => [ + ['email' => 'test@test.de', 'phone' => '+490815',], + ] + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php new file mode 100644 index 0000000000000..6d51ef98517f4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_mailer.php @@ -0,0 +1,30 @@ +loadFromExtension('framework', [ + 'mailer' => [ + 'enabled' => false, + ], + 'messenger' => [ + 'enabled' => true, + ], + 'notifier' => [ + 'enabled' => true, + 'notification_on_failed_messages' => true, + 'chatter_transports' => [ + 'slack' => 'null' + ], + 'texter_transports' => [ + 'twilio' => 'null' + ], + 'channel_policy' => [ + 'low' => ['slack'], + 'high' => ['slack', 'twilio'], + ], + 'admin_recipients' => [ + ['email' => 'test@test.de', 'phone' => '+490815',], + ] + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_messenger.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_messenger.php new file mode 100644 index 0000000000000..454cf5ef7ca81 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_messenger.php @@ -0,0 +1,30 @@ +loadFromExtension('framework', [ + 'mailer' => [ + 'dsn' => 'smtp://example.com', + ], + 'messenger' => [ + 'enabled' => false, + ], + 'notifier' => [ + 'enabled' => true, + 'notification_on_failed_messages' => true, + 'chatter_transports' => [ + 'slack' => 'null' + ], + 'texter_transports' => [ + 'twilio' => 'null' + ], + 'channel_policy' => [ + 'low' => ['slack'], + 'high' => ['slack', 'twilio'], + ], + 'admin_recipients' => [ + ['email' => 'test@test.de', 'phone' => '+490815',], + ] + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_transports.php new file mode 100644 index 0000000000000..9bc87dbee2f58 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/notifier_without_transports.php @@ -0,0 +1,10 @@ +loadFromExtension('framework', [ + 'notifier' => [ + 'enabled' => true, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/php_errors_log_levels.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/php_errors_log_levels.php new file mode 100644 index 0000000000000..620a5871e098f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/php_errors_log_levels.php @@ -0,0 +1,10 @@ +loadFromExtension('framework', [ + 'php_errors' => [ + 'log' => [ + \E_NOTICE => \Psr\Log\LogLevel::ERROR, + \E_WARNING => \Psr\Log\LogLevel::ERROR, + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php index 8f431f8735d89..dc6954fe89da4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/property_accessor.php @@ -3,6 +3,8 @@ $container->loadFromExtension('framework', [ 'property_access' => [ 'magic_call' => true, + 'magic_get' => true, + 'magic_set' => false, 'throw_exception_on_invalid_index' => true, 'throw_exception_on_invalid_property_path' => false, ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session.php index 375008c7db468..8b4c6e6e4c3b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session.php @@ -2,6 +2,7 @@ $container->loadFromExtension('framework', [ 'session' => [ + 'storage_factory_id' => 'session.storage.factory.native', 'handler_id' => null, ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_cookie_secure_auto.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_cookie_secure_auto.php index 7259b07f92615..b52935c726a0f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_cookie_secure_auto.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_cookie_secure_auto.php @@ -2,6 +2,7 @@ $container->loadFromExtension('framework', [ 'session' => [ + 'storage_factory_id' => 'session.storage.factory.native', 'handler_id' => null, 'cookie_secure' => 'auto', ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/template_and_fragments.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/template_and_fragments.php deleted file mode 100644 index 6fd941d9f46c0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/template_and_fragments.php +++ /dev/null @@ -1,18 +0,0 @@ -loadFromExtension('framework', [ - 'templating' => [ - 'cache' => '/path/to/cache', - 'engines' => ['php', 'twig'], - 'loader' => ['loader.foo', 'loader.bar'], - 'form' => [ - 'resources' => ['theme1', 'theme2'], - ], - 'hinclude_default_template' => 'global_hinclude_template', - ], - 'assets' => null, - 'fragments' => [ - 'enabled' => true, - 'hinclude_default_template' => 'global_hinclude_template', - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating.php deleted file mode 100644 index a7edabf763afd..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating.php +++ /dev/null @@ -1,14 +0,0 @@ -loadFromExtension('framework', [ - 'templating' => [ - 'cache' => '/path/to/cache', - 'engines' => ['php', 'twig'], - 'loader' => ['loader.foo', 'loader.bar'], - 'form' => [ - 'resources' => ['theme1', 'theme2'], - ], - 'hinclude_default_template' => 'global_hinclude_template', - ], - 'assets' => null, -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_disabled.php deleted file mode 100644 index 2d5e6d779f79c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_disabled.php +++ /dev/null @@ -1,5 +0,0 @@ -loadFromExtension('framework', [ - 'templating' => false, -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_no_assets.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_no_assets.php deleted file mode 100644 index f4d5a28aafc67..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_no_assets.php +++ /dev/null @@ -1,7 +0,0 @@ -loadFromExtension('framework', [ - 'templating' => [ - 'engines' => ['php', 'twig'], - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_php_assets_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_php_assets_disabled.php deleted file mode 100644 index 851a7e140c747..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/templating_php_assets_disabled.php +++ /dev/null @@ -1,8 +0,0 @@ -loadFromExtension('framework', [ - 'assets' => false, - 'templating' => [ - 'engines' => ['php'], - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php index dff03e398e2dc..933410dfee767 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_annotations.php @@ -7,3 +7,5 @@ 'enable_annotations' => true, ], ]); + +$container->setAlias('validator.alias', 'validator')->setPublic(true); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_and_validation_mode.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_and_validation_mode.php deleted file mode 100644 index 414b323efedef..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_and_validation_mode.php +++ /dev/null @@ -1,8 +0,0 @@ -loadFromExtension('framework', [ - 'validation' => [ - 'strict_email' => true, - 'email_validation_mode' => 'strict', - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_disabled.php deleted file mode 100644 index b5a8b7bba698b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_disabled.php +++ /dev/null @@ -1,7 +0,0 @@ -loadFromExtension('framework', [ - 'validation' => [ - 'strict_email' => false, - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_enabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_enabled.php deleted file mode 100644 index caa47d74700f6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/validation_strict_email_enabled.php +++ /dev/null @@ -1,7 +0,0 @@ -loadFromExtension('framework', [ - 'validation' => [ - 'strict_email' => true, - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow-legacy.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow-legacy.php deleted file mode 100644 index cad94fc773b87..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow-legacy.php +++ /dev/null @@ -1,29 +0,0 @@ -loadFromExtension('framework', [ - 'workflows' => [ - 'legacy' => [ - 'type' => 'state_machine', - 'marking_store' => [ - 'type' => 'single_state', - 'arguments' => [ - 'state', - ], - ], - 'supports' => [ - stdClass::class, - ], - 'initial_place' => 'draft', - 'places' => [ - 'draft', - 'published', - ], - 'transitions' => [ - 'publish' => [ - 'from' => 'draft', - 'to' => 'published', - ], - ], - ], - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_legacy_with_arguments_and_service.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_legacy_with_arguments_and_service.php deleted file mode 100644 index 003b99f210973..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_legacy_with_arguments_and_service.php +++ /dev/null @@ -1,31 +0,0 @@ -loadFromExtension('framework', [ - 'workflows' => [ - 'my_workflow' => [ - 'marking_store' => [ - 'arguments' => ['a', 'b'], - 'service' => 'workflow_service', - ], - 'supports' => [ - FrameworkExtensionTest::class, - ], - 'places' => [ - 'first', - 'last', - ], - 'transitions' => [ - 'go' => [ - 'from' => [ - 'first', - ], - 'to' => [ - 'last', - ], - ], - ], - ], - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_legacy_with_type_and_service.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_legacy_with_type_and_service.php deleted file mode 100644 index 15189349bd83d..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_legacy_with_type_and_service.php +++ /dev/null @@ -1,31 +0,0 @@ -loadFromExtension('framework', [ - 'workflows' => [ - 'my_workflow' => [ - 'marking_store' => [ - 'type' => 'method', - 'service' => 'workflow_service', - ], - 'supports' => [ - FrameworkExtensionTest::class, - ], - 'places' => [ - 'first', - 'last', - ], - 'transitions' => [ - 'go' => [ - 'from' => [ - 'first', - ], - 'to' => [ - 'last', - ], - ], - ], - ], - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_no_events_to_dispatch.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_no_events_to_dispatch.php new file mode 100644 index 0000000000000..e4eefd4c28440 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_no_events_to_dispatch.php @@ -0,0 +1,42 @@ +loadFromExtension('framework', [ + 'workflows' => [ + 'my_workflow' => [ + 'type' => 'state_machine', + 'marking_store' => [ + 'type' => 'method', + 'property' => 'state', + ], + 'supports' => [ + FrameworkExtensionTest::class, + ], + 'events_to_dispatch' => [], + 'places' => [ + 'one', + 'two', + 'three', + ], + 'transitions' => [ + 'count_to_two' => [ + 'from' => [ + 'one', + ], + 'to' => [ + 'two', + ], + ], + 'count_to_three' => [ + 'from' => [ + 'two', + ], + 'to' => [ + 'three', + ], + ], + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_specified_events_to_dispatch.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_specified_events_to_dispatch.php new file mode 100644 index 0000000000000..0fc5c29c8b43e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_specified_events_to_dispatch.php @@ -0,0 +1,45 @@ +loadFromExtension('framework', [ + 'workflows' => [ + 'my_workflow' => [ + 'type' => 'state_machine', + 'marking_store' => [ + 'type' => 'method', + 'property' => 'state', + ], + 'supports' => [ + FrameworkExtensionTest::class, + ], + 'events_to_dispatch' => [ + 'workflow.leave', + 'workflow.completed', + ], + 'places' => [ + 'one', + 'two', + 'three', + ], + 'transitions' => [ + 'count_to_two' => [ + 'from' => [ + 'one', + ], + 'to' => [ + 'two', + ], + ], + 'count_to_three' => [ + 'from' => [ + 'two', + ], + 'to' => [ + 'three', + ], + ], + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled.php index e6f261f8f2f7a..f048de1ceb5ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled.php @@ -10,8 +10,8 @@ 'places' => ['bar', 'baz'], 'transitions' => [ 'bar_baz' => [ - 'from' => ['foo'], - 'to' => ['bar'], + 'from' => ['bar'], + 'to' => ['baz'], ], ], ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled_named_workflows.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled_named_workflows.php index e4bc05a66f46f..f79a2d10e97f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled_named_workflows.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows_explicitly_enabled_named_workflows.php @@ -10,8 +10,8 @@ 'places' => ['bar', 'baz'], 'transitions' => [ 'bar_baz' => [ - 'from' => ['foo'], - 'to' => ['bar'], + 'from' => ['bar'], + 'to' => ['baz'], ], ], ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml index 7ae57afaab679..dadee4529d8b5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/assets.xml @@ -22,6 +22,15 @@ https://bar_version_strategy.example.com + + + + + + + https://cdn.example.com/manifest.json + https://cdn.example.com/manifest.json + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml index d53e0764f8db3..7c75178c8cf0a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml @@ -8,16 +8,22 @@ - - + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_app_redis_tag_aware.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_app_redis_tag_aware.xml new file mode 100644 index 0000000000000..2929e87e200e8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_app_redis_tag_aware.xml @@ -0,0 +1,13 @@ + + + + + + cache.adapter.redis_tag_aware + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_app_redis_tag_aware_pool.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_app_redis_tag_aware_pool.xml new file mode 100644 index 0000000000000..65c06a1da6df7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_app_redis_tag_aware_pool.xml @@ -0,0 +1,14 @@ + + + + + + cache.redis_tag_aware.foo + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml index 4cd628eadc15a..24acb3e32707c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/csrf.xml @@ -8,7 +8,7 @@ - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml new file mode 100644 index 0000000000000..cc73b8de3ced6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml index 1bdf2e528432e..30fcf6b7f3929 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml @@ -8,7 +8,7 @@ - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml index c04193e837b34..1e89bca965ea2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml @@ -8,7 +8,7 @@ - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_default_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_default_csrf.xml new file mode 100644 index 0000000000000..9ed40084722b8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_default_csrf.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml index 092174a2d9720..3af5322be212f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_no_csrf.xml @@ -7,7 +7,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 8c4c489ea3430..2e115a5aebbc0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -7,15 +7,17 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> + fr + en - + - - + + text/csv @@ -31,7 +33,11 @@ - + + + true + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_mock_response_factory.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_mock_response_factory.xml new file mode 100644 index 0000000000000..6835b2f4b7660 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_mock_response_factory.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_retry.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_retry.xml new file mode 100644 index 0000000000000..eb7798914488b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_retry.xml @@ -0,0 +1,29 @@ + + + + + + + + + + GET + HEAD + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_translator_enabled_locales.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_translator_enabled_locales.xml new file mode 100644 index 0000000000000..91139d9d0af3f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/legacy_translator_enabled_locales.xml @@ -0,0 +1,17 @@ + + + + + + + + %kernel.project_dir%/Fixtures/translations + fr + en + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_disabled_message_bus.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_disabled_message_bus.xml new file mode 100644 index 0000000000000..e6d3a47e38a93 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_disabled_message_bus.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml index ff4d75c8250bf..be53f59bc3cad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml @@ -13,6 +13,9 @@ redirected@example.org redirected1@example.org + from@example.org + bcc1@example.org + bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_specific_message_bus.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_specific_message_bus.xml new file mode 100644 index 0000000000000..116ba032a03a3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_specific_message_bus.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml index a6eb67dc81024..cbe538d33e99c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml @@ -15,6 +15,9 @@ redirected@example.org redirected1@example.org + from@example.org + bcc1@example.org + bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml new file mode 100644 index 0000000000000..b8e9f19759429 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml new file mode 100644 index 0000000000000..c6e5c530fda1b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml index 837db14c1cad4..b0510d580ceaf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml @@ -20,6 +20,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_disabled_reset_on_message.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_disabled_reset_on_message.xml new file mode 100644 index 0000000000000..67a2a414e8fcf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_disabled_reset_on_message.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_explict_reset_on_message_legacy.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_explict_reset_on_message_legacy.xml new file mode 100644 index 0000000000000..1451bb66f516d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_with_explict_reset_on_message_legacy.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml new file mode 100644 index 0000000000000..47e2e2b0c1b13 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier.xml @@ -0,0 +1,21 @@ + + + + + + + + null + null + slack + twilio + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml new file mode 100644 index 0000000000000..1c62b5265b897 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_mailer.xml @@ -0,0 +1,21 @@ + + + + + + + + null + null + slack + twilio + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_messenger.xml new file mode 100644 index 0000000000000..c2a5134762588 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_messenger.xml @@ -0,0 +1,21 @@ + + + + + + + + null + null + slack + twilio + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_transports.xml new file mode 100644 index 0000000000000..a1ec7863cda1a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/notifier_without_transports.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/php_errors_log_levels.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/php_errors_log_levels.xml new file mode 100644 index 0000000000000..1b6642a575c4c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/php_errors_log_levels.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml index 07e33ae3e8d96..9406919e92394 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/property_accessor.xml @@ -7,6 +7,6 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session.xml index 599cbdee1ccc0..e91d51955e6fa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session.xml @@ -7,6 +7,6 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_cookie_secure_auto.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_cookie_secure_auto.xml index 1fff3e090e88f..3023c43fc13ad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_cookie_secure_auto.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_cookie_secure_auto.xml @@ -7,6 +7,6 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/template_and_fragments.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/template_and_fragments.xml deleted file mode 100644 index d253e9f5deeed..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/template_and_fragments.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - loader.foo - loader.bar - php - twig - - theme1 - theme2 - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating.xml deleted file mode 100644 index 192533dbf617a..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - loader.foo - loader.bar - php - twig - - theme1 - theme2 - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_disabled.xml deleted file mode 100644 index 8fda04d92a591..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_disabled.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_no_assets.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_no_assets.xml deleted file mode 100644 index 7bba936c5a58e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_no_assets.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - php - twig - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_php_translator_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_php_translator_disabled.xml deleted file mode 100644 index 8e0a8cc039cb5..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_php_translator_disabled.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - php - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_php_translator_enabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_php_translator_enabled.xml deleted file mode 100644 index d1cb1fad83367..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/templating_php_translator_enabled.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - php - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml index f993a20d97314..2324b9ca6e374 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_annotations.xml @@ -9,4 +9,8 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_and_validation_mode.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_and_validation_mode.xml deleted file mode 100644 index a88a37f26b03e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_and_validation_mode.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_disabled.xml deleted file mode 100644 index fa41396161130..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_disabled.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_enabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_enabled.xml deleted file mode 100644 index ec5c6b94e6592..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/validation_strict_email_enabled.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow-legacy.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow-legacy.xml deleted file mode 100644 index 5a46d24d0df08..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow-legacy.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - state - - stdClass - - - - draft - published - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_legacy_with_arguments_and_service.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_legacy_with_arguments_and_service.xml deleted file mode 100644 index cdb2f997357ee..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_legacy_with_arguments_and_service.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - a - a - - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - - - - a - a - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_legacy_with_type_and_service.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_legacy_with_type_and_service.xml deleted file mode 100644 index e137f9b4b041b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_legacy_with_type_and_service.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - - - - a - a - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_no_events_to_dispatch.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_no_events_to_dispatch.xml new file mode 100644 index 0000000000000..2f563da4bf96b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_no_events_to_dispatch.xml @@ -0,0 +1,28 @@ + + + + + + + one + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + + + + + + one + two + + + two + three + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_specified_events_to_dispatch.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_specified_events_to_dispatch.xml new file mode 100644 index 0000000000000..d442828f8cfbf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_specified_events_to_dispatch.xml @@ -0,0 +1,29 @@ + + + + + + + one + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + workflow.leave + workflow.completed + + + + + one + two + + + two + three + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml index 290ab50e7d8da..79361af57a61f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml @@ -8,6 +8,7 @@ + draft Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest @@ -42,6 +43,7 @@ + start Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows_explicitly_enabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows_explicitly_enabled.xml index bb544d8979bb2..af93d44e18387 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows_explicitly_enabled.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows_explicitly_enabled.xml @@ -6,7 +6,7 @@ http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest bar baz diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml index a1679e389ddbf..cfd4f07b04346 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/assets.yml @@ -19,3 +19,16 @@ framework: version_strategy: assets.custom_version_strategy json_manifest_strategy: json_manifest_path: '/path/to/manifest.json' + remote_manifest: + json_manifest_path: 'https://cdn.example.com/manifest.json' + var_manifest: + json_manifest_path: '%var_json_manifest_path%' + env_manifest: + json_manifest_path: '%env(env_manifest)%' + strict_manifest_strategy: + json_manifest_path: '/path/to/manifest.json' + strict_mode: true + +parameters: + var_json_manifest_path: 'https://cdn.example.com/manifest.json' + env(env_manifest): https://cdn.example.com/manifest.json diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml index c6c6383715744..c89c027f5aecf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml @@ -4,10 +4,6 @@ framework: cache.foo: adapter: cache.adapter.apcu default_lifetime: 30 - cache.bar: - adapter: cache.adapter.doctrine - default_lifetime: 5 - provider: app.doctrine_cache_provider cache.baz: adapter: cache.adapter.filesystem default_lifetime: 7 @@ -16,7 +12,9 @@ framework: default_lifetime: 10 provider: app.cache_pool cache.def: - default_lifetime: 11 + default_lifetime: PT11S + cache.expr: + default_lifetime: 13 seconds cache.chain: default_lifetime: 12 adapter: @@ -27,3 +25,18 @@ framework: adapter: cache.adapter.array default_lifetime: 410 tags: true + cache.redis_tag_aware.foo: + adapter: cache.adapter.redis_tag_aware + cache.redis_tag_aware.foo2: + tags: true + adapter: cache.adapter.redis_tag_aware + cache.redis_tag_aware.bar: + adapter: cache.redis_tag_aware.foo + cache.redis_tag_aware.bar2: + tags: true + adapter: cache.redis_tag_aware.foo + cache.redis_tag_aware.baz: + adapter: cache.redis_tag_aware.foo2 + cache.redis_tag_aware.baz2: + tags: true + adapter: cache.redis_tag_aware.foo2 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_app_redis_tag_aware.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_app_redis_tag_aware.yml new file mode 100644 index 0000000000000..b1c89adafa0ca --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_app_redis_tag_aware.yml @@ -0,0 +1,3 @@ +framework: + cache: + app: cache.adapter.redis_tag_aware diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_app_redis_tag_aware_pool.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_app_redis_tag_aware_pool.yml new file mode 100644 index 0000000000000..9eb8b83c775c5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_app_redis_tag_aware_pool.yml @@ -0,0 +1,6 @@ +framework: + cache: + app: cache.redis_tag_aware.foo + pools: + cache.redis_tag_aware.foo: + adapter: cache.adapter.redis_tag_aware diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml index dbdd4951946fa..643e7bda4554a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/csrf.yml @@ -1,5 +1,5 @@ framework: secret: s3cr3t csrf_protection: ~ - form: ~ - session: ~ + session: + storage_factory_id: session.storage.factory.native diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml new file mode 100644 index 0000000000000..82fab4e04a9f9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml @@ -0,0 +1,5 @@ +framework: + exceptions: + Symfony\Component\HttpKernel\Exception\BadRequestHttpException: + log_level: info + status_code: 422 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_default_csrf.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_default_csrf.yml new file mode 100644 index 0000000000000..5036cc8577735 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_default_csrf.yml @@ -0,0 +1,6 @@ +framework: + form: + legacy_error_messages: false + session: + storage_factory_id: session.storage.factory.native + handler_id: null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index a189f992daf34..6da130bbec556 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -1,6 +1,7 @@ framework: secret: s3cr3t default_locale: fr + enabled_locales: ['fr', 'en'] csrf_protection: true form: csrf_protection: @@ -16,8 +17,9 @@ framework: router: resource: '%kernel.project_dir%/config/routing.xml' type: xml + utf8: true session: - storage_id: session.storage.native + storage_factory_id: session.storage.factory.native handler_id: session.handler.native_file name: _SYMFONY cookie_lifetime: 86400 @@ -52,6 +54,8 @@ framework: name_converter: serializer.name_converter.camel_case_to_snake_case circular_reference_handler: my.circular.reference.handler max_depth_handler: my.max.depth.handler + default_context: + enable_max_depth: true property_info: ~ ide: file%%link%%format request: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_mock_response_factory.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_mock_response_factory.yml new file mode 100644 index 0000000000000..b958591084136 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_mock_response_factory.yml @@ -0,0 +1,4 @@ +framework: + http_client: + default_options: ~ + mock_response_factory: my_response_factory diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml new file mode 100644 index 0000000000000..eba686819c300 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_retry.yml @@ -0,0 +1,18 @@ +framework: + http_client: + default_options: + retry_failed: + retry_strategy: null + http_codes: + 429: true + 500: ['GET', 'HEAD'] + max_retries: 2 + delay: 100 + multiplier: 2 + max_delay: 0 + jitter: 0.3 + scoped_clients: + foo: + base_uri: http://example.com + retry_failed: + multiplier: 4 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_disabled_message_bus.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_disabled_message_bus.yml new file mode 100644 index 0000000000000..f941f7c8c4f6b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_disabled_message_bus.yml @@ -0,0 +1,4 @@ +framework: + mailer: + dsn: 'smtp://example.com' + message_bus: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml index 07d435d9df30b..f8b3c87c4302c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml @@ -6,3 +6,7 @@ framework: recipients: - redirected@example.org - redirected1@example.org + headers: + from: from@example.org + bcc: [bcc1@example.org, bcc2@example.org] + foo: bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_specific_message_bus.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_specific_message_bus.yml new file mode 100644 index 0000000000000..ddfc7a479a49d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_specific_message_bus.yml @@ -0,0 +1,4 @@ +framework: + mailer: + dsn: 'smtp://example.com' + message_bus: app.another_bus diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml index 6035988d76e59..bc4657d3a4397 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml @@ -8,3 +8,7 @@ framework: recipients: - redirected@example.org - redirected1@example.org + headers: + from: from@example.org + bcc: [bcc1@example.org, bcc2@example.org] + foo: bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml new file mode 100644 index 0000000000000..863f18a7d1a1f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml @@ -0,0 +1,12 @@ +framework: + messenger: + transports: + transport_1: + dsn: 'null://' + failure_transport: failure_transport_1 + transport_2: 'null://' + transport_3: + dsn: 'null://' + failure_transport: failure_transport_3 + failure_transport_1: 'null://' + failure_transport_3: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml new file mode 100644 index 0000000000000..10023edb0b9fd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml @@ -0,0 +1,14 @@ +framework: + messenger: + failure_transport: failure_transport_global + transports: + transport_1: + dsn: 'null://' + failure_transport: failure_transport_1 + transport_2: 'null://' + transport_3: + dsn: 'null://' + failure_transport: failure_transport_3 + failure_transport_global: 'null://' + failure_transport_1: 'null://' + failure_transport_3: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml index daab75bd87e40..d00f4a65dd37c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml @@ -19,3 +19,4 @@ framework: max_delay: 100 failed: 'in-memory:///' redis: 'redis://127.0.0.1:6379/messages' + beanstalkd: 'beanstalkd://127.0.0.1:11300' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_disabled_reset_on_message.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_disabled_reset_on_message.yml new file mode 100644 index 0000000000000..f67395c85c191 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_disabled_reset_on_message.yml @@ -0,0 +1,10 @@ +framework: + messenger: + reset_on_message: false + routing: + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz'] + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo' + transports: + sender.biz: 'null://' + sender.bar: 'null://' + sender.foo: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_explict_reset_on_message_legacy.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_explict_reset_on_message_legacy.yml new file mode 100644 index 0000000000000..3bf374f474c75 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_with_explict_reset_on_message_legacy.yml @@ -0,0 +1,10 @@ +framework: + messenger: + reset_on_message: true + routing: + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz'] + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo' + transports: + sender.biz: 'null://' + sender.bar: 'null://' + sender.foo: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml new file mode 100644 index 0000000000000..586cb98a4a138 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier.yml @@ -0,0 +1,17 @@ +framework: + messenger: + enabled: true + mailer: + dsn: 'smtp://example.com' + notifier: + enabled: true + notification_on_failed_messages: true + chatter_transports: + slack: 'null' + texter_transports: + twilio: 'null' + channel_policy: + low: ['slack'] + high: ['slack', 'twilio'] + admin_recipients: + - { email: 'test@test.de', phone: '+490815' } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml new file mode 100644 index 0000000000000..75fa3cf889825 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_mailer.yml @@ -0,0 +1,17 @@ +framework: + mailer: + enabled: false + messenger: + enabled: true + notifier: + enabled: true + notification_on_failed_messages: true + chatter_transports: + slack: 'null' + texter_transports: + twilio: 'null' + channel_policy: + low: ['slack'] + high: ['slack', 'twilio'] + admin_recipients: + - { email: 'test@test.de', phone: '+490815' } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_messenger.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_messenger.yml new file mode 100644 index 0000000000000..93d1f0aa190a7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_messenger.yml @@ -0,0 +1,17 @@ +framework: + mailer: + dsn: 'smtp://example.com' + messenger: + enabled: false + notifier: + enabled: true + notification_on_failed_messages: true + chatter_transports: + slack: 'null' + texter_transports: + twilio: 'null' + channel_policy: + low: ['slack'] + high: ['slack', 'twilio'] + admin_recipients: + - { email: 'test@test.de', phone: '+490815' } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_transports.yml new file mode 100644 index 0000000000000..856b0cd7c7a0e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/notifier_without_transports.yml @@ -0,0 +1,3 @@ +framework: + notifier: + enabled: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/php_errors_log_levels.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/php_errors_log_levels.yml new file mode 100644 index 0000000000000..ad9fd30667de2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/php_errors_log_levels.yml @@ -0,0 +1,5 @@ +framework: + php_errors: + log: + !php/const \E_NOTICE: !php/const Psr\Log\LogLevel::ERROR + !php/const \E_WARNING: !php/const Psr\Log\LogLevel::ERROR diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml index ea527c9821116..931b50383f210 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/property_accessor.yml @@ -1,5 +1,7 @@ framework: property_access: magic_call: true + magic_get: true + magic_set: false throw_exception_on_invalid_index: true throw_exception_on_invalid_property_path: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session.yml index d91b0c3147dfd..eb0df8d01c76c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session.yml @@ -1,3 +1,4 @@ framework: session: + storage_factory_id: session.storage.factory.native handler_id: null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_cookie_secure_auto.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_cookie_secure_auto.yml index 17fe2f5a02c03..739b49b1e6ab9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_cookie_secure_auto.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_cookie_secure_auto.yml @@ -1,4 +1,5 @@ framework: session: + storage_factory_id: session.storage.factory.native handler_id: ~ cookie_secure: auto diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/template_and_fragments.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/template_and_fragments.yml deleted file mode 100644 index dbf7b697541e0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/template_and_fragments.yml +++ /dev/null @@ -1,12 +0,0 @@ -framework: - fragments: - enabled: true - hinclude_default_template: global_hinclude_template - templating: - engines: [php, twig] - loader: [loader.foo, loader.bar] - cache: /path/to/cache - form: - resources: [theme1, theme2] - hinclude_default_template: global_hinclude_template - assets: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating.yml deleted file mode 100644 index d307e1609b090..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating.yml +++ /dev/null @@ -1,9 +0,0 @@ -framework: - templating: - engines: [php, twig] - loader: [loader.foo, loader.bar] - cache: /path/to/cache - form: - resources: [theme1, theme2] - hinclude_default_template: global_hinclude_template - assets: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_disabled.yml deleted file mode 100644 index 1e548b859473c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_disabled.yml +++ /dev/null @@ -1,2 +0,0 @@ -framework: - templating: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_no_assets.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_no_assets.yml deleted file mode 100644 index 393477aeb49ac..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_no_assets.yml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - templating: - engines: [php, twig] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_assets_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_assets_disabled.yml deleted file mode 100644 index 7ef6b3e57c292..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_assets_disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - assets: false - templating: - engines: [php] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_translator_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_translator_disabled.yml deleted file mode 100644 index fe0f3e83b5683..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_translator_disabled.yml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - translator: false - templating: - engines: [php] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_translator_enabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_translator_enabled.yml deleted file mode 100644 index 0991a2007d77f..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/templating_php_translator_enabled.yml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - translator: true - templating: - engines: [php] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml index 41f17969b83ca..97b433f8cfb08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_annotations.yml @@ -3,3 +3,8 @@ framework: validation: enabled: true enable_annotations: true + +services: + validator.alias: + alias: validator + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_and_validation_mode.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_and_validation_mode.yml deleted file mode 100644 index 64e658ba69436..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_and_validation_mode.yml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - validation: - strict_email: true - email_validation_mode: html5 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_disabled.yml deleted file mode 100644 index b5be5f598b14c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_disabled.yml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - validation: - strict_email: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_enabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_enabled.yml deleted file mode 100644 index 1c805f9b923d2..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/validation_strict_email_enabled.yml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - validation: - strict_email: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow-legacy.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow-legacy.yml deleted file mode 100644 index 19f51dc9f4119..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow-legacy.yml +++ /dev/null @@ -1,18 +0,0 @@ -framework: - workflows: - legacy: - type: state_machine - marking_store: - type: single_state - arguments: - - state - initial_place: draft - supports: - - stdClass - places: - - draft - - published - transitions: - publish: - from: draft - to: published diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_legacy_with_arguments_and_service.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_legacy_with_arguments_and_service.yml deleted file mode 100644 index a46d4b67e6b24..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_legacy_with_arguments_and_service.yml +++ /dev/null @@ -1,19 +0,0 @@ -framework: - workflows: - my_workflow: - marking_store: - arguments: - - a - - b - service: workflow_service - supports: - - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - places: - - first - - last - transitions: - go: - from: - - first - to: - - last diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_legacy_with_type_and_service.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_legacy_with_type_and_service.yml deleted file mode 100644 index 33ee68b1bc810..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_legacy_with_type_and_service.yml +++ /dev/null @@ -1,17 +0,0 @@ -framework: - workflows: - my_workflow: - marking_store: - type: method - service: workflow_service - supports: - - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest - places: - - first - - last - transitions: - go: - from: - - first - to: - - last diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_no_events_to_dispatch.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_no_events_to_dispatch.yml new file mode 100644 index 0000000000000..e0a281f27db46 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_no_events_to_dispatch.yml @@ -0,0 +1,22 @@ +framework: + workflows: + my_workflow: + type: state_machine + initial_marking: one + events_to_dispatch: [] + marking_store: + type: method + property: state + supports: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + places: + - one + - two + - three + transitions: + count_to_two: + from: one + to: two + count_to_three: + from: two + to: three diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_specified_events_to_dispatch.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_specified_events_to_dispatch.yml new file mode 100644 index 0000000000000..d5ff3d5e5fe0d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_specified_events_to_dispatch.yml @@ -0,0 +1,22 @@ +framework: + workflows: + my_workflow: + type: state_machine + initial_marking: one + events_to_dispatch: ['workflow.leave', 'workflow.completed'] + marking_store: + type: method + property: state + supports: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + places: + - one + - two + - three + transitions: + count_to_two: + from: one + to: two + count_to_three: + from: two + to: three diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled.yml index bee231736b233..bbecf4bfc420e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled.yml @@ -12,5 +12,5 @@ framework: - baz transitions: bar_baz: - from: [foo] + from: [bar] to: [bar] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled_named_workflows.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled_named_workflows.yml index c0462c9ab8c9d..786e3bcad292e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled_named_workflows.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows_explicitly_enabled_named_workflows.yml @@ -11,5 +11,5 @@ framework: - baz transitions: bar_baz: - from: [foo] - to: [bar] + from: [bar] + to: [baz] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index b66d0837c3a37..248eeacd0102a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,7 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; -use Doctrine\Common\Annotations\PsrCachedReader; +use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; @@ -23,26 +23,32 @@ use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; -use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Finder\Finder; use Symfony\Component\Form\Form; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; +use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Security\Core\Security; @@ -53,18 +59,20 @@ use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\FormErrorNormalizer; use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; -use Symfony\Component\Validator\Util\LegacyTranslatorProxy; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Workflow; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Component\Workflow\WorkflowEvents; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; abstract class FrameworkExtensionTest extends TestCase { @@ -89,18 +97,16 @@ public function testPropertyAccessWithDefaultValue() $container = $this->createContainerFromFile('full'); $def = $container->getDefinition('property_accessor'); - $this->assertFalse($def->getArgument(0)); - $this->assertFalse($def->getArgument(1)); - $this->assertTrue($def->getArgument(3)); + $this->assertSame(PropertyAccessor::MAGIC_SET | PropertyAccessor::MAGIC_GET, $def->getArgument(0)); + $this->assertSame(PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH, $def->getArgument(1)); } public function testPropertyAccessWithOverriddenValues() { $container = $this->createContainerFromFile('property_accessor'); $def = $container->getDefinition('property_accessor'); - $this->assertTrue($def->getArgument(0)); - $this->assertTrue($def->getArgument(1)); - $this->assertFalse($def->getArgument(3)); + $this->assertSame(PropertyAccessor::MAGIC_GET | PropertyAccessor::MAGIC_CALL, $def->getArgument(0)); + $this->assertSame(PropertyAccessor::THROW_ON_INVALID_INDEX, $def->getArgument(1)); } public function testPropertyAccessCache() @@ -147,6 +153,18 @@ public function testCsrfProtectionForFormsEnablesCsrfProtectionAutomatically() $this->assertTrue($container->hasDefinition('security.csrf.token_manager')); } + public function testFormsCsrfIsEnabledByDefault() + { + if (class_exists(FullStack::class)) { + $this->markTestSkipped('testing with the FullStack prevents verifying default values'); + } + $container = $this->createContainerFromFile('form_default_csrf'); + + $this->assertTrue($container->hasDefinition('security.csrf.token_manager')); + $this->assertTrue($container->hasParameter('form.type_extension.csrf.enabled')); + $this->assertTrue($container->getParameter('form.type_extension.csrf.enabled')); + } + public function testHttpMethodOverride() { $container = $this->createContainerFromFile('full'); @@ -170,18 +188,11 @@ public function testEsiDisabled() $this->assertFalse($container->hasDefinition('esi')); } - /** - * @group legacy - */ - public function testAmbiguousWhenBothTemplatingAndFragments() - { - $this->expectException(\LogicException::class); - $this->createContainerFromFile('template_and_fragments'); - } - public function testFragmentsAndHinclude() { $container = $this->createContainerFromFile('fragments_and_hinclude'); + $this->assertTrue($container->has('fragment.uri_generator')); + $this->assertTrue($container->hasAlias(FragmentUriGeneratorInterface::class)); $this->assertTrue($container->hasParameter('fragment.renderer.hinclude.global_template')); $this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template')); } @@ -233,6 +244,14 @@ public function testWorkflows() $this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service'); $this->assertSame('workflow.abstract', $container->getDefinition('workflow.article')->getParent()); + + $args = $container->getDefinition('workflow.article')->getArguments(); + $this->assertArrayHasKey('index_0', $args); + $this->assertArrayHasKey('index_1', $args); + $this->assertArrayHasKey('index_3', $args); + $this->assertArrayHasKey('index_4', $args); + $this->assertNull($args['index_4'], 'Workflows has eventsToDispatch=null'); + $this->assertTrue($container->hasDefinition('workflow.article.definition'), 'Workflow definition is registered as a service'); $workflowDefinition = $container->getDefinition('workflow.article.definition'); @@ -251,7 +270,7 @@ public function testWorkflows() ); $this->assertCount(4, $workflowDefinition->getArgument(1)); $this->assertSame(['draft'], $workflowDefinition->getArgument(2)); - $metadataStoreDefinition = $workflowDefinition->getArgument(3); + $metadataStoreDefinition = $container->getDefinition('workflow.article.metadata_store'); $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $this->assertSame([ 'title' => 'article workflow', @@ -279,8 +298,12 @@ public function testWorkflows() $this->assertCount(9, $stateMachineDefinition->getArgument(1)); $this->assertSame(['start'], $stateMachineDefinition->getArgument(2)); - $metadataStoreDefinition = $stateMachineDefinition->getArgument(3); - $this->assertInstanceOf(Definition::class, $metadataStoreDefinition); + $metadataStoreReference = $stateMachineDefinition->getArgument(3); + $this->assertInstanceOf(Reference::class, $metadataStoreReference); + $this->assertSame('state_machine.pull_request.metadata_store', (string) $metadataStoreReference); + + $metadataStoreDefinition = $container->getDefinition('state_machine.pull_request.metadata_store'); + $this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $workflowMetadata = $metadataStoreDefinition->getArgument(0); @@ -297,7 +320,7 @@ public function testWorkflows() $params = $transitionsMetadataCall[1]; $this->assertCount(2, $params); $this->assertInstanceOf(Reference::class, $params[0]); - $this->assertSame('state_machine.pull_request.transition.0', (string) $params[0]); + $this->assertSame('.state_machine.pull_request.transition.0', (string) $params[0]); $serviceMarkingStoreWorkflowDefinition = $container->getDefinition('workflow.service_marking_store_workflow'); /** @var Reference $markingStoreRef */ @@ -310,33 +333,6 @@ public function testWorkflows() $this->assertGreaterThan(0, \count($registryDefinition->getMethodCalls())); } - /** - * @group legacy - */ - public function testWorkflowLegacy() - { - $container = $this->createContainerFromFile('workflow-legacy'); - - $this->assertTrue($container->hasDefinition('state_machine.legacy'), 'Workflow is registered as a service'); - $this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.legacy')->getParent()); - $this->assertTrue($container->hasDefinition('state_machine.legacy.definition'), 'Workflow definition is registered as a service'); - - $workflowDefinition = $container->getDefinition('state_machine.legacy.definition'); - - $this->assertSame(['draft'], $workflowDefinition->getArgument(2)); - - $this->assertSame( - [ - 'draft', - 'published', - ], - $workflowDefinition->getArgument(0), - 'Places are passed to the workflow definition' - ); - - $this->assertSame(['workflow.definition' => [['name' => 'legacy', 'type' => 'state_machine']]], $workflowDefinition->getTags()); - } - public function testWorkflowAreValidated() { $this->expectException(InvalidDefinitionException::class); @@ -344,13 +340,6 @@ public function testWorkflowAreValidated() $this->createContainerFromFile('workflow_not_valid'); } - public function testWorkflowCannotHaveBothTypeAndService() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('"type" and "service" cannot be used together.'); - $this->createContainerFromFile('workflow_legacy_with_type_and_service'); - } - public function testWorkflowCannotHaveBothSupportsAndSupportStrategy() { $this->expectException(InvalidConfigurationException::class); @@ -365,16 +354,6 @@ public function testWorkflowShouldHaveOneOfSupportsAndSupportStrategy() $this->createContainerFromFile('workflow_without_support_and_support_strategy'); } - /** - * @group legacy - */ - public function testWorkflowCannotHaveBothArgumentsAndService() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('"arguments" and "service" cannot be used together.'); - $this->createContainerFromFile('workflow_legacy_with_arguments_and_service'); - } - public function testWorkflowMultipleTransitionsWithSameName() { $container = $this->createContainerFromFile('workflow_with_multiple_transitions_with_same_name'); @@ -388,7 +367,7 @@ public function testWorkflowMultipleTransitionsWithSameName() $this->assertCount(5, $transitions); - $this->assertSame('workflow.article.transition.0', (string) $transitions[0]); + $this->assertSame('.workflow.article.transition.0', (string) $transitions[0]); $this->assertSame([ 'request_review', [ @@ -399,7 +378,7 @@ public function testWorkflowMultipleTransitionsWithSameName() ], ], $container->getDefinition($transitions[0])->getArguments()); - $this->assertSame('workflow.article.transition.1', (string) $transitions[1]); + $this->assertSame('.workflow.article.transition.1', (string) $transitions[1]); $this->assertSame([ 'journalist_approval', [ @@ -410,7 +389,7 @@ public function testWorkflowMultipleTransitionsWithSameName() ], ], $container->getDefinition($transitions[1])->getArguments()); - $this->assertSame('workflow.article.transition.2', (string) $transitions[2]); + $this->assertSame('.workflow.article.transition.2', (string) $transitions[2]); $this->assertSame([ 'spellchecker_approval', [ @@ -421,7 +400,7 @@ public function testWorkflowMultipleTransitionsWithSameName() ], ], $container->getDefinition($transitions[2])->getArguments()); - $this->assertSame('workflow.article.transition.3', (string) $transitions[3]); + $this->assertSame('.workflow.article.transition.3', (string) $transitions[3]); $this->assertSame([ 'publish', [ @@ -433,7 +412,7 @@ public function testWorkflowMultipleTransitionsWithSameName() ], ], $container->getDefinition($transitions[3])->getArguments()); - $this->assertSame('workflow.article.transition.4', (string) $transitions[4]); + $this->assertSame('.workflow.article.transition.4', (string) $transitions[4]); $this->assertSame([ 'publish', [ @@ -449,10 +428,10 @@ public function testWorkflowGuardExpressions() { $container = $this->createContainerFromFile('workflow_with_guard_expression'); - $this->assertTrue($container->hasDefinition('workflow.article.listener.guard'), 'Workflow guard listener is registered as a service'); + $this->assertTrue($container->hasDefinition('.workflow.article.listener.guard'), 'Workflow guard listener is registered as a service'); $this->assertTrue($container->hasParameter('workflow.has_guard_listeners'), 'Workflow guard listeners parameter exists'); $this->assertTrue(true === $container->getParameter('workflow.has_guard_listeners'), 'Workflow guard listeners parameter is enabled'); - $guardDefinition = $container->getDefinition('workflow.article.listener.guard'); + $guardDefinition = $container->getDefinition('.workflow.article.listener.guard'); $this->assertSame([ [ 'event' => 'workflow.article.guard.publish', @@ -462,9 +441,9 @@ public function testWorkflowGuardExpressions() $guardsConfiguration = $guardDefinition->getArgument(0); $this->assertTrue(1 === \count($guardsConfiguration), 'Workflow guard configuration contains one element per transition name'); $transitionGuardExpressions = $guardsConfiguration['workflow.article.guard.publish']; - $this->assertSame('workflow.article.transition.3', (string) $transitionGuardExpressions[0]->getArgument(0)); + $this->assertSame('.workflow.article.transition.3', (string) $transitionGuardExpressions[0]->getArgument(0)); $this->assertSame('!!true', $transitionGuardExpressions[0]->getArgument(1)); - $this->assertSame('workflow.article.transition.4', (string) $transitionGuardExpressions[1]->getArgument(0)); + $this->assertSame('.workflow.article.transition.4', (string) $transitionGuardExpressions[1]->getArgument(0)); $this->assertSame('!!false', $transitionGuardExpressions[1]->getArgument(1)); } @@ -476,26 +455,44 @@ public function testWorkflowServicesCanBeEnabled() $this->assertTrue($container->hasDefinition('console.command.workflow_dump')); } - public function testExplicitlyEnabledWorkflows() + public function testWorkflowsExplicitlyEnabled() { $container = $this->createContainerFromFile('workflows_explicitly_enabled'); $this->assertTrue($container->hasDefinition('workflow.foo.definition')); } - public function testExplicitlyEnabledWorkflowNamedWorkflows() + public function testWorkflowsNamedExplicitlyEnabled() { $container = $this->createContainerFromFile('workflows_explicitly_enabled_named_workflows'); $this->assertTrue($container->hasDefinition('workflow.workflows.definition')); } + public function testWorkflowsWithNoDispatchedEvents() + { + $container = $this->createContainerFromFile('workflow_with_no_events_to_dispatch'); + + $eventsToDispatch = $container->getDefinition('state_machine.my_workflow')->getArgument('index_4'); + + $this->assertSame([], $eventsToDispatch); + } + + public function testWorkflowsWithSpecifiedDispatchedEvents() + { + $container = $this->createContainerFromFile('workflow_with_specified_events_to_dispatch'); + + $eventsToDispatch = $container->getDefinition('state_machine.my_workflow')->getArgument('index_4'); + + $this->assertSame([WorkflowEvents::LEAVE, WorkflowEvents::COMPLETED], $eventsToDispatch); + } + public function testEnabledPhpErrorsConfig() { $container = $this->createContainerFromFile('php_errors_enabled'); $definition = $container->getDefinition('debug.debug_handlers_listener'); - $this->assertEquals(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(1)); + $this->assertEquals(new Reference('monolog.logger.php', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(1)); $this->assertNull($definition->getArgument(2)); $this->assertSame(-1, $container->getParameter('debug.error_handler.throw_at')); } @@ -515,10 +512,34 @@ public function testPhpErrorsWithLogLevel() $container = $this->createContainerFromFile('php_errors_log_level'); $definition = $container->getDefinition('debug.debug_handlers_listener'); - $this->assertEquals(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(1)); + $this->assertEquals(new Reference('monolog.logger.php', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(1)); $this->assertSame(8, $definition->getArgument(2)); } + public function testPhpErrorsWithLogLevels() + { + $container = $this->createContainerFromFile('php_errors_log_levels'); + + $definition = $container->getDefinition('debug.debug_handlers_listener'); + $this->assertEquals(new Reference('monolog.logger.php', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(1)); + $this->assertSame([ + \E_NOTICE => \Psr\Log\LogLevel::ERROR, + \E_WARNING => \Psr\Log\LogLevel::ERROR, + ], $definition->getArgument(2)); + } + + public function testExceptionsConfig() + { + $container = $this->createContainerFromFile('exceptions'); + + $this->assertSame([ + \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class => [ + 'log_level' => 'info', + 'status_code' => 422, + ], + ], $container->getDefinition('exception_listener')->getArgument(3)); + } + public function testRouter() { $container = $this->createContainerFromFile('full'); @@ -528,6 +549,8 @@ public function testRouter() $this->assertEquals($container->getParameter('kernel.project_dir').'/config/routing.xml', $container->getParameter('router.resource'), '->registerRouterConfiguration() sets routing resource'); $this->assertEquals('%router.resource%', $arguments[1], '->registerRouterConfiguration() sets routing resource'); $this->assertEquals('xml', $arguments[2]['resource_type'], '->registerRouterConfiguration() sets routing resource type'); + + $this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2)); } public function testRouterRequiresResourceOption() @@ -542,9 +565,8 @@ public function testSession() { $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('session'), '->registerSessionConfiguration() loads session.xml'); $this->assertEquals('fr', $container->getParameter('kernel.default_locale')); - $this->assertEquals('session.storage.native', (string) $container->getAlias('session.storage')); + $this->assertEquals('session.storage.factory.native', (string) $container->getAlias('session.storage.factory')); $this->assertEquals('session.handler.native_file', (string) $container->getAlias('session.handler')); $options = $container->getParameter('session.storage.options'); @@ -568,13 +590,13 @@ public function testNullSessionHandler() { $container = $this->createContainerFromFile('session'); - $this->assertTrue($container->hasDefinition('session'), '->registerSessionConfiguration() loads session.xml'); - $this->assertNull($container->getDefinition('session.storage.native')->getArgument(1)); - $this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0)); + $this->assertNull($container->getDefinition('session.storage.factory.native')->getArgument(1)); + $this->assertNull($container->getDefinition('session.storage.factory.php_bridge')->getArgument(0)); $this->assertSame('session.handler.native_file', (string) $container->getAlias('session.handler')); - $expected = ['session', 'initialized_session']; + $expected = ['session_factory', 'logger', 'session_collector']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); + $this->assertFalse($container->getDefinition('session.storage.factory.native')->getArgument(3)); } public function testRequest() @@ -593,40 +615,6 @@ public function testEmptyRequestFormats() $this->assertFalse($container->hasDefinition('request.add_request_formats_listener'), '->registerRequestConfiguration() does not load request.xml when no request formats are defined'); } - /** - * @group legacy - */ - public function testTemplating() - { - $container = $this->createContainerFromFile('templating'); - - $this->assertTrue($container->hasDefinition('templating.name_parser'), '->registerTemplatingConfiguration() loads templating.xml'); - - $this->assertEquals('templating.engine.delegating', (string) $container->getAlias('templating'), '->registerTemplatingConfiguration() configures delegating loader if multiple engines are provided'); - - $this->assertEquals($container->getDefinition('templating.loader.chain'), $container->getDefinition('templating.loader.wrapped'), '->registerTemplatingConfiguration() configures loader chain if multiple loaders are provided'); - - $this->assertEquals($container->getDefinition('templating.loader'), $container->getDefinition('templating.loader.cache'), '->registerTemplatingConfiguration() configures the loader to use cache'); - - $this->assertEquals('%templating.loader.cache.path%', $container->getDefinition('templating.loader.cache')->getArgument(1)); - $this->assertEquals('/path/to/cache', $container->getParameter('templating.loader.cache.path')); - - $this->assertEquals(['php', 'twig'], $container->getParameter('templating.engines'), '->registerTemplatingConfiguration() sets a templating.engines parameter'); - - $this->assertEquals(['FrameworkBundle:Form', 'theme1', 'theme2'], $container->getParameter('templating.helper.form.resources'), '->registerTemplatingConfiguration() registers the theme and adds the base theme'); - $this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template'), '->registerTemplatingConfiguration() registers the global hinclude.js template'); - } - - /** - * @group legacy - */ - public function testTemplatingCanBeDisabled() - { - $container = $this->createContainerFromFile('templating_disabled'); - - $this->assertFalse($container->hasParameter('templating.engines'), '"templating.engines" container parameter is not registered when templating is disabled.'); - } - public function testAssets() { $container = $this->createContainerFromFile('assets'); @@ -637,8 +625,13 @@ public function testAssets() $this->assertUrlPackage($container, $defaultPackage, ['http://cdn.example.com'], 'SomeVersionScheme', '%%s?version=%%s'); // packages - $packages = $packages->getArgument(1); - $this->assertCount(6, $packages); + $packageTags = $container->findTaggedServiceIds('assets.package'); + $this->assertCount(10, $packageTags); + + $packages = []; + foreach ($packageTags as $serviceId => $tagAttributes) { + $packages[$tagAttributes[0]['package']] = $serviceId; + } $package = $container->getDefinition((string) $packages['images_path']); $this->assertPathPackage($container, $package, '/foo', 'SomeVersionScheme', '%%s?version=%%s'); @@ -659,6 +652,30 @@ public function testAssets() $versionStrategy = $container->getDefinition((string) $package->getArgument(1)); $this->assertEquals('assets.json_manifest_version_strategy', $versionStrategy->getParent()); $this->assertEquals('/path/to/manifest.json', $versionStrategy->getArgument(0)); + $this->assertFalse($versionStrategy->getArgument(2)); + + $package = $container->getDefinition($packages['remote_manifest']); + $versionStrategy = $container->getDefinition($package->getArgument(1)); + $this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent()); + $this->assertSame('https://cdn.example.com/manifest.json', $versionStrategy->getArgument(0)); + + $package = $container->getDefinition($packages['var_manifest']); + $versionStrategy = $container->getDefinition($package->getArgument(1)); + $this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent()); + $this->assertSame('https://cdn.example.com/manifest.json', $versionStrategy->getArgument(0)); + $this->assertFalse($versionStrategy->getArgument(2)); + + $package = $container->getDefinition($packages['env_manifest']); + $versionStrategy = $container->getDefinition($package->getArgument(1)); + $this->assertSame('assets.json_manifest_version_strategy', $versionStrategy->getParent()); + $this->assertStringMatchesFormat('env_%s', $versionStrategy->getArgument(0)); + $this->assertFalse($versionStrategy->getArgument(2)); + + $package = $container->getDefinition((string) $packages['strict_manifest_strategy']); + $versionStrategy = $container->getDefinition((string) $package->getArgument(1)); + $this->assertEquals('assets.json_manifest_version_strategy', $versionStrategy->getParent()); + $this->assertEquals('/path/to/manifest.json', $versionStrategy->getArgument(0)); + $this->assertTrue($versionStrategy->getArgument(2)); } public function testAssetsDefaultVersionStrategyAsService() @@ -671,13 +688,6 @@ public function testAssetsDefaultVersionStrategyAsService() $this->assertEquals('assets.custom_version_strategy', (string) $defaultPackage->getArgument(1)); } - public function testAssetsCanBeDisabled() - { - $container = $this->createContainerFromFile('assets_disabled'); - - $this->assertFalse($container->has('templating.helper.assets'), 'The templating.helper.assets helper service is removed when assets are disabled.'); - } - public function testWebLink() { $container = $this->createContainerFromFile('web_link'); @@ -701,14 +711,112 @@ public function testMessenger() { $container = $this->createContainerFromFile('messenger'); $this->assertTrue($container->hasDefinition('console.command.messenger_consume_messages')); - $this->assertTrue($container->hasAlias('message_bus')); - $this->assertTrue($container->getAlias('message_bus')->isPublic()); $this->assertTrue($container->hasAlias('messenger.default_bus')); $this->assertTrue($container->getAlias('messenger.default_bus')->isPublic()); $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); $this->assertTrue($container->hasDefinition('messenger.transport_factory')); $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); + $this->assertTrue($container->hasDefinition('messenger.listener.reset_services')); + $this->assertSame('messenger.listener.reset_services', (string) $container->getDefinition('console.command.messenger_consume_messages')->getArgument(5)); + } + + public function testMessengerWithoutConsole() + { + $extension = $this->createPartialMock(FrameworkExtension::class, ['hasConsole', 'getAlias']); + $extension->method('hasConsole')->willReturn(false); + $extension->method('getAlias')->willReturn((new FrameworkExtension())->getAlias()); + + $container = $this->createContainerFromFile('messenger', [], true, false, $extension); + $container->compile(); + + $this->assertFalse($container->hasDefinition('console.command.messenger_consume_messages')); + $this->assertTrue($container->hasAlias('messenger.default_bus')); + $this->assertTrue($container->getAlias('messenger.default_bus')->isPublic()); + $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport_factory')); + $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); + $this->assertFalse($container->hasDefinition('messenger.listener.reset_services')); + } + + public function testMessengerMultipleFailureTransports() + { + $container = $this->createContainerFromFile('messenger_multiple_failure_transports'); + + $failureTransport1Definition = $container->getDefinition('messenger.transport.failure_transport_1'); + $failureTransport1Tags = $failureTransport1Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_1', + 'is_failure_transport' => true, + ], $failureTransport1Tags); + + $failureTransport3Definition = $container->getDefinition('messenger.transport.failure_transport_3'); + $failureTransport3Tags = $failureTransport3Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_3', + 'is_failure_transport' => true, + ], $failureTransport3Tags); + + // transport 2 exists but does not appear in the mapping + $this->assertFalse($container->hasDefinition('messenger.transport.failure_transport_2')); + + $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0); + $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0); + $expectedTransportsByFailureTransports = [ + 'transport_1' => new Reference('messenger.transport.failure_transport_1'), + 'transport_3' => new Reference('messenger.transport.failure_transport_3'), + ]; + + $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) { + $values = $serviceClosureArgument->getValues(); + + return array_shift($values); + }, $failureTransports); + $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences); + } + + public function testMessengerMultipleFailureTransportsWithGlobalFailureTransport() + { + $container = $this->createContainerFromFile('messenger_multiple_failure_transports_global'); + + $this->assertEquals('messenger.transport.failure_transport_global', (string) $container->getAlias('messenger.failure_transports.default')); + + $failureTransport1Definition = $container->getDefinition('messenger.transport.failure_transport_1'); + $failureTransport1Tags = $failureTransport1Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_1', + 'is_failure_transport' => true, + ], $failureTransport1Tags); + + $failureTransport3Definition = $container->getDefinition('messenger.transport.failure_transport_3'); + $failureTransport3Tags = $failureTransport3Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_3', + 'is_failure_transport' => true, + ], $failureTransport3Tags); + + $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0); + $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0); + $expectedTransportsByFailureTransports = [ + 'failure_transport_1' => new Reference('messenger.transport.failure_transport_global'), + 'failure_transport_3' => new Reference('messenger.transport.failure_transport_global'), + 'failure_transport_global' => new Reference('messenger.transport.failure_transport_global'), + 'transport_1' => new Reference('messenger.transport.failure_transport_1'), + 'transport_2' => new Reference('messenger.transport.failure_transport_global'), + 'transport_3' => new Reference('messenger.transport.failure_transport_3'), + ]; + + $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) { + $values = $serviceClosureArgument->getValues(); + + return array_shift($values); + }, $failureTransports); + $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences); } public function testMessengerTransports() @@ -716,7 +824,8 @@ public function testMessengerTransports() $container = $this->createContainerFromFile('messenger_transports'); $this->assertTrue($container->hasDefinition('messenger.transport.default')); $this->assertTrue($container->getDefinition('messenger.transport.default')->hasTag('messenger.receiver')); - $this->assertEquals([['alias' => 'default']], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver')); + $this->assertEquals([ + ['alias' => 'default', 'is_failure_transport' => false], ], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver')); $transportArguments = $container->getDefinition('messenger.transport.default')->getArguments(); $this->assertEquals(new Reference('messenger.default_serializer'), $transportArguments[2]); @@ -742,12 +851,37 @@ public function testMessengerTransports() $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.beanstalkd')); + $transportFactory = $container->getDefinition('messenger.transport.beanstalkd')->getFactory(); + $transportArguments = $container->getDefinition('messenger.transport.beanstalkd')->getArguments(); + + $this->assertEquals([new Reference('messenger.transport_factory'), 'createTransport'], $transportFactory); + $this->assertCount(3, $transportArguments); + $this->assertSame('beanstalkd://127.0.0.1:11300', $transportArguments[0]); + + $this->assertTrue($container->hasDefinition('messenger.transport.beanstalkd.factory')); + $this->assertSame(10, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(0)); $this->assertSame(7, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(1)); $this->assertSame(3, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(2)); $this->assertSame(100, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(3)); - $this->assertEquals(new Reference('messenger.transport.failed'), $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0)); + $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0); + $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0); + $expectedTransportsByFailureTransports = [ + 'beanstalkd' => new Reference('messenger.transport.failed'), + 'customised' => new Reference('messenger.transport.failed'), + 'default' => new Reference('messenger.transport.failed'), + 'failed' => new Reference('messenger.transport.failed'), + 'redis' => new Reference('messenger.transport.failed'), + ]; + + $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) { + $values = $serviceClosureArgument->getValues(); + + return array_shift($values); + }, $failureTransports); + $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences); } public function testMessengerRouting() @@ -815,8 +949,6 @@ public function testMessengerWithMultipleBuses() ['id' => 'handle_message', 'arguments' => []], ], $container->getParameter('messenger.bus.queries.middleware')); - $this->assertTrue($container->hasAlias('message_bus')); - $this->assertSame('messenger.bus.commands', (string) $container->getAlias('message_bus')); $this->assertTrue($container->hasAlias('messenger.default_bus')); $this->assertSame('messenger.bus.commands', (string) $container->getAlias('messenger.default_bus')); } @@ -838,7 +970,7 @@ public function testMessengerInvalidTransportRouting() public function testTranslator() { $container = $this->createContainerFromFile('full'); - $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.xml'); + $this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.php'); $this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator'); $options = $container->getDefinition('translator.default')->getArgument(4); @@ -908,20 +1040,6 @@ function ($directory) { $this->assertSame('Fixtures/translations', $options['cache_vary']['scanned_directories'][3]); } - /** - * @group legacy - * @expectedDeprecation Translations directory "%s/Resources/translations" is deprecated since Symfony 4.2, use "%s/translations" instead. - */ - public function testLegacyTranslationsDirectory() - { - $container = $this->createContainerFromFile('full', ['kernel.root_dir' => __DIR__.'/Fixtures']); - $options = $container->getDefinition('translator.default')->getArgument(4); - $files = array_map('realpath', $options['resource_files']['en']); - - $dir = str_replace('/', \DIRECTORY_SEPARATOR, __DIR__.'/Fixtures/Resources/translations/test_default.en.xlf'); - $this->assertContains($dir, $files, '->registerTranslatorConfiguration() finds translation resources in legacy directory'); - } - public function testTranslatorMultipleFallbacks() { $container = $this->createContainerFromFile('translator_fallbacks'); @@ -937,17 +1055,6 @@ public function testTranslatorCacheDirDisabled() $this->assertNull($options['cache_dir']); } - /** - * @group legacy - */ - public function testTemplatingRequiresAtLeastOneEngine() - { - $this->expectException(InvalidConfigurationException::class); - $container = $this->createContainer(); - $loader = new FrameworkExtension(); - $loader->load([['templating' => null]], $container); - } - public function testValidation() { $container = $this->createContainerFromFile('full'); @@ -963,15 +1070,11 @@ public function testValidation() $annotations = !class_exists(FullStack::class) && class_exists(Annotation::class); - $this->assertCount($annotations ? 7 : 6, $calls); + $this->assertCount($annotations ? 8 : 6, $calls); $this->assertSame('setConstraintValidatorFactory', $calls[0][0]); $this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]); $this->assertSame('setTranslator', $calls[1][0]); - if (interface_exists(TranslatorInterface::class) && class_exists(LegacyTranslatorProxy::class)) { - $this->assertEquals([new Definition(LegacyTranslatorProxy::class, [new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])], $calls[1][1]); - } else { - $this->assertEquals([new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)], $calls[1][1]); - } + $this->assertEquals([new Reference('translator', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)], $calls[1][1]); $this->assertSame('setTranslationDomain', $calls[2][0]); $this->assertSame(['%validator.translation_domain%'], $calls[2][1]); $this->assertSame('addXmlMappings', $calls[3][0]); @@ -979,6 +1082,7 @@ public function testValidation() $i = 3; if ($annotations) { $this->assertSame('enableAnnotationMapping', $calls[++$i][0]); + $this->assertSame('setDoctrineAnnotationReader', $calls[++$i][0]); } $this->assertSame('addMethodMapping', $calls[++$i][0]); $this->assertSame(['loadValidatorMetadata'], $calls[$i][1]); @@ -990,7 +1094,7 @@ public function testValidationService() { $container = $this->createContainerFromFile('validation_annotations', ['kernel.charset' => 'UTF-8'], false); - $this->assertInstanceOf(ValidatorInterface::class, $container->get('validator')); + $this->assertInstanceOf(ValidatorInterface::class, $container->get('validator.alias')); } public function testAnnotations() @@ -1000,11 +1104,7 @@ public function testAnnotations() $container->compile(); $this->assertEquals($container->getParameter('kernel.cache_dir').'/annotations', $container->getDefinition('annotations.filesystem_cache_adapter')->getArgument(2)); - if (class_exists(PsrCachedReader::class)) { - $this->assertSame('annotations.filesystem_cache_adapter', (string) $container->getDefinition('annotation_reader')->getArgument(1)); - } else { - $this->assertSame('annotations.filesystem_cache', (string) $container->getDefinition('annotation_reader')->getArgument(1)); - } + $this->assertSame('annotations.filesystem_cache_adapter', (string) $container->getDefinition('annotation_reader')->getArgument(1)); } public function testFileLinkFormat() @@ -1024,13 +1124,14 @@ public function testValidationAnnotations() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $this->assertCount(7, $calls); + $this->assertCount(8, $calls); $this->assertSame('enableAnnotationMapping', $calls[4][0]); - $this->assertEquals([new Reference('annotation_reader')], $calls[4][1]); - $this->assertSame('addMethodMapping', $calls[5][0]); - $this->assertSame(['loadValidatorMetadata'], $calls[5][1]); - $this->assertSame('setMappingCache', $calls[6][0]); - $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[6][1]); + $this->assertSame('setDoctrineAnnotationReader', $calls[5][0]); + $this->assertEquals([new Reference('annotation_reader')], $calls[5][1]); + $this->assertSame('addMethodMapping', $calls[6][0]); + $this->assertSame(['loadValidatorMetadata'], $calls[6][1]); + $this->assertSame('setMappingCache', $calls[7][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]); // no cache this time } @@ -1045,14 +1146,15 @@ public function testValidationPaths() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $this->assertCount(8, $calls); + $this->assertCount(9, $calls); $this->assertSame('addXmlMappings', $calls[3][0]); $this->assertSame('addYamlMappings', $calls[4][0]); $this->assertSame('enableAnnotationMapping', $calls[5][0]); - $this->assertSame('addMethodMapping', $calls[6][0]); - $this->assertSame(['loadValidatorMetadata'], $calls[6][1]); - $this->assertSame('setMappingCache', $calls[7][0]); - $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]); + $this->assertSame('setDoctrineAnnotationReader', $calls[6][0]); + $this->assertSame('addMethodMapping', $calls[7][0]); + $this->assertSame(['loadValidatorMetadata'], $calls[7][1]); + $this->assertSame('setMappingCache', $calls[8][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[8][1]); $xmlMappings = $calls[3][1][0]; $this->assertCount(3, $xmlMappings); @@ -1105,50 +1207,18 @@ public function testValidationNoStaticMethod() $annotations = !class_exists(FullStack::class) && class_exists(Annotation::class); - $this->assertCount($annotations ? 6 : 5, $calls); + $this->assertCount($annotations ? 7 : 5, $calls); $this->assertSame('addXmlMappings', $calls[3][0]); $i = 3; if ($annotations) { $this->assertSame('enableAnnotationMapping', $calls[++$i][0]); + $this->assertSame('setDoctrineAnnotationReader', $calls[++$i][0]); } $this->assertSame('setMappingCache', $calls[++$i][0]); $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[$i][1]); // no cache, no annotations, no static methods } - /** - * @group legacy - * @expectedDeprecation The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead. - */ - public function testCannotConfigureStrictEmailAndEmailValidationModeAtTheSameTime() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('"strict_email" and "email_validation_mode" cannot be used together.'); - $this->createContainerFromFile('validation_strict_email_and_validation_mode'); - } - - /** - * @group legacy - * @expectedDeprecation The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead. - */ - public function testEnabledStrictEmailOptionIsMappedToStrictEmailValidationMode() - { - $container = $this->createContainerFromFile('validation_strict_email_enabled'); - - $this->assertSame('strict', $container->getDefinition('validator.email')->getArgument(0)); - } - - /** - * @group legacy - * @expectedDeprecation The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead. - */ - public function testDisabledStrictEmailOptionIsMappedToLooseEmailValidationMode() - { - $container = $this->createContainerFromFile('validation_strict_email_disabled'); - - $this->assertSame('loose', $container->getDefinition('validator.email')->getArgument(0)); - } - public function testEmailValidationModeIsPassedToEmailValidator() { $container = $this->createContainerFromFile('validation_email_validation_mode'); @@ -1251,7 +1321,7 @@ public function testRegisterSerializerExtractor() $serializerExtractorDefinition = $container->getDefinition('property_info.serializer_extractor'); $this->assertEquals('serializer.mapping.class_metadata_factory', $serializerExtractorDefinition->getArgument(0)->__toString()); - $this->assertFalse($serializerExtractorDefinition->isPublic()); + $this->assertTrue(!$serializerExtractorDefinition->isPublic() || $serializerExtractorDefinition->isPrivate()); $tag = $serializerExtractorDefinition->getTag('property_info.list_extractor'); $this->assertEquals(['priority' => -999], $tag[0]); } @@ -1289,6 +1359,17 @@ public function testDateTimeNormalizerRegistered() $this->assertEquals(-910, $tag[0]['priority']); } + public function testFormErrorNormalizerRegistred() + { + $container = $this->createContainerFromFile('full'); + + $definition = $container->getDefinition('serializer.normalizer.form_error'); + $tag = $definition->getTag('serializer.normalizer'); + + $this->assertEquals(FormErrorNormalizer::class, $definition->getClass()); + $this->assertEquals(-915, $tag[0]['priority']); + } + public function testJsonSerializableNormalizerRegistered() { $container = $this->createContainerFromFile('full'); @@ -1333,7 +1414,7 @@ public function testSerializerCacheActivated() $this->assertEquals(new Reference('serializer.mapping.cache.symfony'), $cache); } - public function testSerializerCacheDisabled() + public function testSerializerCacheNotActivatedDebug() { $container = $this->createContainerFromFile('serializer_enabled', ['kernel.debug' => true, 'kernel.container_class' => __CLASS__]); $this->assertFalse($container->hasDefinition('serializer.mapping.cache_class_metadata_factory')); @@ -1345,7 +1426,7 @@ public function testSerializerMapping() $projectDir = $container->getParameter('kernel.project_dir'); $configDir = __DIR__.'/Fixtures/TestBundle/Resources/config'; $expectedLoaders = [ - new Definition(AnnotationLoader::class, [new Reference('annotation_reader')]), + new Definition(AnnotationLoader::class, [new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE)]), new Definition(XmlFileLoader::class, [$configDir.'/serialization.xml']), new Definition(YamlFileLoader::class, [$configDir.'/serialization.yml']), new Definition(YamlFileLoader::class, [$projectDir.'/config/serializer/foo.yml']), @@ -1371,45 +1452,6 @@ public function testSerializerMapping() $this->assertEquals($expectedLoaders, $loaders); } - /** - * @group legacy - */ - public function testAssetHelperWhenAssetsAreEnabled() - { - $container = $this->createContainerFromFile('templating'); - $packages = $container->getDefinition('templating.helper.assets')->getArgument(0); - - $this->assertSame('assets.packages', (string) $packages); - } - - /** - * @group legacy - */ - public function testAssetHelperWhenTemplatesAreEnabledAndNoAssetsConfiguration() - { - $container = $this->createContainerFromFile('templating_no_assets'); - $packages = $container->getDefinition('templating.helper.assets')->getArgument(0); - - $this->assertSame('assets.packages', (string) $packages); - } - - /** - * @group legacy - */ - public function testAssetsHelperIsRemovedWhenPhpTemplatingEngineIsEnabledAndAssetsAreDisabled() - { - $container = $this->createContainerFromFile('templating_php_assets_disabled'); - - $this->assertTrue(!$container->has('templating.helper.assets'), 'The templating.helper.assets helper service is removed when assets are disabled.'); - } - - public function testAssetHelperWhenAssetsAndTemplatesAreDisabled() - { - $container = $this->createContainerFromFile('default_config'); - - $this->assertFalse($container->hasDefinition('templating.helper.assets')); - } - public function testSerializerServiceIsRegisteredWhenEnabled() { $container = $this->createContainerFromFile('serializer_enabled'); @@ -1482,10 +1524,10 @@ public function testCachePoolServices() $container->compile(); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foo', 'cache.adapter.apcu', 30); - $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.bar', 'cache.adapter.doctrine', 5); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.baz', 'cache.adapter.filesystem', 7); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foobar', 'cache.adapter.psr6', 10); - $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.def', 'cache.app', 11); + $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.def', 'cache.app', 'PT11S'); + $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.expr', 'cache.app', '13 seconds'); $chain = $container->getDefinition('cache.chain'); @@ -1524,6 +1566,67 @@ public function testCachePoolServices() } } + public function testRedisTagAwareAdapter() + { + $container = $this->createContainerFromFile('cache', [], true); + + $aliasesForArguments = []; + $argNames = [ + 'cacheRedisTagAwareFoo', + 'cacheRedisTagAwareFoo2', + 'cacheRedisTagAwareBar', + 'cacheRedisTagAwareBar2', + 'cacheRedisTagAwareBaz', + 'cacheRedisTagAwareBaz2', + ]; + foreach ($argNames as $argumentName) { + $aliasesForArguments[] = sprintf('%s $%s', TagAwareCacheInterface::class, $argumentName); + $aliasesForArguments[] = sprintf('%s $%s', CacheInterface::class, $argumentName); + $aliasesForArguments[] = sprintf('%s $%s', CacheItemPoolInterface::class, $argumentName); + } + + foreach ($aliasesForArguments as $aliasForArgumentStr) { + $aliasForArgument = $container->getAlias($aliasForArgumentStr); + $this->assertNotNull($aliasForArgument, sprintf("No alias found for '%s'", $aliasForArgumentStr)); + + $def = $container->getDefinition((string) $aliasForArgument); + $this->assertInstanceOf(ChildDefinition::class, $def, sprintf("No definition found for '%s'", $aliasForArgumentStr)); + + $defParent = $container->getDefinition($def->getParent()); + if ($defParent instanceof ChildDefinition) { + $defParent = $container->getDefinition($defParent->getParent()); + } + + $this->assertSame(RedisTagAwareAdapter::class, $defParent->getClass(), sprintf("'%s' is not %s", $aliasForArgumentStr, RedisTagAwareAdapter::class)); + } + } + + /** + * @dataProvider appRedisTagAwareConfigProvider + */ + public function testAppRedisTagAwareAdapter(string $configFile) + { + $container = $this->createContainerFromFile($configFile); + + foreach ([TagAwareCacheInterface::class, CacheInterface::class, CacheItemPoolInterface::class] as $alias) { + $def = $container->findDefinition($alias); + + while ($def instanceof ChildDefinition) { + $def = $container->getDefinition($def->getParent()); + } + + $this->assertSame(RedisTagAwareAdapter::class, $def->getClass()); + } + } + + public function appRedisTagAwareConfigProvider(): array + { + return [ + ['cache_app_redis_tag_aware'], + ['cache_app_redis_tag_aware_pool'], + ]; + } + public function testRemovesResourceCheckerConfigCacheFactoryArgumentOnlyIfNoDebug() { $container = $this->createContainer(['kernel.debug' => true]); @@ -1555,7 +1658,7 @@ public function testSessionCookieSecureAuto() { $container = $this->createContainerFromFile('session_cookie_secure_auto'); - $expected = ['session', 'initialized_session', 'session_storage', 'request_stack']; + $expected = ['session_factory', 'logger', 'session_collector']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); } @@ -1621,6 +1724,24 @@ public function testHttpClientOverrideDefaultOptions() $this->assertSame($expected, $container->getDefinition('foo')->getArgument(2)); } + public function testHttpClientRetry() + { + if (!class_exists(RetryableHttpClient::class)) { + $this->expectException(LogicException::class); + } + $container = $this->createContainerFromFile('http_client_retry'); + + $this->assertSame([429, 500 => ['GET', 'HEAD']], $container->getDefinition('http_client.retry_strategy')->getArgument(0)); + $this->assertSame(100, $container->getDefinition('http_client.retry_strategy')->getArgument(1)); + $this->assertSame(2, $container->getDefinition('http_client.retry_strategy')->getArgument(2)); + $this->assertSame(0, $container->getDefinition('http_client.retry_strategy')->getArgument(3)); + $this->assertSame(0.3, $container->getDefinition('http_client.retry_strategy')->getArgument(4)); + $this->assertSame(2, $container->getDefinition('http_client.retryable')->getArgument(2)); + + $this->assertSame(RetryableHttpClient::class, $container->getDefinition('foo.retryable')->getClass()); + $this->assertSame(4, $container->getDefinition('foo.retry_strategy')->getArgument(2)); + } + public function testHttpClientWithQueryParameterKey() { $container = $this->createContainerFromFile('http_client_xml_key'); @@ -1691,6 +1812,42 @@ public function testMailer(string $configFile, array $expectedTransports) $l = $container->getDefinition('mailer.envelope_listener'); $this->assertSame('sender@example.org', $l->getArgument(0)); $this->assertSame(['redirected@example.org', 'redirected1@example.org'], $l->getArgument(1)); + $this->assertEquals(new Reference('messenger.default_bus', ContainerInterface::NULL_ON_INVALID_REFERENCE), $container->getDefinition('mailer.mailer')->getArgument(1)); + + $this->assertTrue($container->hasDefinition('mailer.message_listener')); + $l = $container->getDefinition('mailer.message_listener'); + $h = $l->getArgument(0); + $this->assertCount(3, $h->getMethodCalls()); + } + + public function testMailerWithDisabledMessageBus() + { + $container = $this->createContainerFromFile('mailer_with_disabled_message_bus'); + + $this->assertNull($container->getDefinition('mailer.mailer')->getArgument(1)); + } + + public function testMailerWithSpecificMessageBus() + { + $container = $this->createContainerFromFile('mailer_with_specific_message_bus'); + + $this->assertEquals(new Reference('app.another_bus'), $container->getDefinition('mailer.mailer')->getArgument(1)); + } + + public function testHttpClientMockResponseFactory() + { + $container = $this->createContainerFromFile('http_client_mock_response_factory'); + + $definition = $container->getDefinition('http_client.mock_client'); + + $this->assertSame(MockHttpClient::class, $definition->getClass()); + $this->assertCount(1, $definition->getArguments()); + + $argument = $definition->getArgument(0); + + $this->assertInstanceOf(Reference::class, $argument); + $this->assertSame('http_client', current($definition->getDecoratedService())); + $this->assertSame('my_response_factory', (string) $argument); } public function testRegisterParameterCollectingBehaviorDescribingTags() @@ -1702,22 +1859,74 @@ public function testRegisterParameterCollectingBehaviorDescribingTags() 'container.service_locator', 'container.service_subscriber', 'kernel.event_subscriber', + 'kernel.event_listener', 'kernel.locale_aware', 'kernel.reset', ], $container->getParameter('container.behavior_describing_tags')); } + public function testNotifierWithoutMailer() + { + $container = $this->createContainerFromFile('notifier_without_mailer'); + + $this->assertFalse($container->hasDefinition('notifier.channel.email')); + } + + public function testNotifierWithoutMessenger() + { + $container = $this->createContainerFromFile('notifier_without_messenger'); + + $this->assertFalse($container->getDefinition('notifier.failed_message_listener')->hasTag('kernel.event_subscriber')); + } + + public function testNotifierWithMailerAndMessenger() + { + $container = $this->createContainerFromFile('notifier'); + + $this->assertTrue($container->hasDefinition('notifier')); + $this->assertTrue($container->hasDefinition('chatter')); + $this->assertTrue($container->hasDefinition('texter')); + $this->assertTrue($container->hasDefinition('notifier.channel.chat')); + $this->assertTrue($container->hasDefinition('notifier.channel.email')); + $this->assertTrue($container->hasDefinition('notifier.channel.sms')); + $this->assertTrue($container->hasDefinition('notifier.channel_policy')); + $this->assertTrue($container->getDefinition('notifier.failed_message_listener')->hasTag('kernel.event_subscriber')); + } + + public function testNotifierWithoutTransports() + { + $container = $this->createContainerFromFile('notifier_without_transports'); + + $this->assertTrue($container->hasDefinition('notifier')); + $this->assertFalse($container->hasDefinition('chatter')); + $this->assertFalse($container->hasDefinition('texter')); + } + + public function testIfNotifierTransportsAreKnownByFrameworkExtension() + { + if (!class_exists(FullStack::class)) { + $this->markTestSkipped('This test can only run in fullstack test suites'); + } + + $container = $this->createContainerFromFile('notifier'); + + foreach ((new Finder())->in(\dirname(__DIR__, 4).'/Component/Notifier/Bridge')->directories()->depth(0)->exclude('Mercure') as $bridgeDirectory) { + $transportFactoryName = strtolower(preg_replace('/(.)([A-Z])/', '$1-$2', $bridgeDirectory->getFilename())); + $this->assertTrue($container->hasDefinition('notifier.transport_factory.'.$transportFactoryName), sprintf('Did you forget to add the "%s" TransportFactory to the $classToServices array in FrameworkExtension?', $bridgeDirectory->getFilename())); + } + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], 'kernel.bundles_metadata' => ['FrameworkBundle' => ['namespace' => 'Symfony\\Bundle\\FrameworkBundle', 'path' => __DIR__.'/../..']], 'kernel.cache_dir' => __DIR__, + 'kernel.build_dir' => __DIR__, 'kernel.project_dir' => __DIR__, 'kernel.debug' => false, 'kernel.environment' => 'test', 'kernel.name' => 'kernel', - 'kernel.root_dir' => __DIR__, 'kernel.container_class' => 'testContainer', 'container.build_hash' => 'Abc1234', 'container.build_id' => hash('crc32', 'Abc123423456789'), @@ -1725,14 +1934,14 @@ protected function createContainer(array $data = []) ], $data))); } - protected function createContainerFromFile($file, $data = [], $resetCompilerPasses = true, $compile = true) + protected function createContainerFromFile($file, $data = [], $resetCompilerPasses = true, $compile = true, FrameworkExtension $extension = null) { $cacheKey = md5(static::class.$file.serialize($data)); if ($compile && isset(self::$containerCache[$cacheKey])) { return self::$containerCache[$cacheKey]; } $container = $this->createContainer($data); - $container->registerExtension(new FrameworkExtension()); + $container->registerExtension($extension ?: new FrameworkExtension()); $this->loadFromFile($container, $file); if ($resetCompilerPasses) { @@ -1818,9 +2027,6 @@ private function assertCachePoolServiceDefinitionIsCreated(ContainerBuilder $con case 'cache.adapter.apcu': $this->assertSame(ApcuAdapter::class, $parentDefinition->getClass()); break; - case 'cache.adapter.doctrine': - $this->assertSame(DoctrineAdapter::class, $parentDefinition->getClass()); - break; case 'cache.app': case 'cache.adapter.filesystem': $this->assertSame(FilesystemAdapter::class, $parentDefinition->getClass()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 1c69d35e4e813..d9f4b1192f456 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -13,6 +13,8 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; +use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; @@ -83,111 +85,49 @@ public function testWorkflowValidationStateMachine() }); } - /** - * @group legacy - * @expectedDeprecation Using a workflow with type=workflow and a marking_store=single_state is deprecated since Symfony 4.3. Use type=state_machine instead. - */ - public function testWorkflowDeprecateWorkflowSingleState() + public function testRateLimiterWithLockFactory() { - $this->createContainerFromClosure(function ($container) { - $container->loadFromExtension('framework', [ - 'workflows' => [ - 'article' => [ - 'type' => 'workflow', - 'marking_store' => [ - 'type' => 'single_state', - ], - 'supports' => [ - __CLASS__, - ], - 'places' => [ - 'a', - 'b', - 'c', - ], - 'transitions' => [ - 'a_to_b' => [ - 'from' => ['a'], - 'to' => ['b'], - ], - ], + try { + $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'lock' => false, + 'rate_limiter' => [ + 'with_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], ], - ], - ]); - }); - } + ]); + }); - /** - * @group legacy - */ - public function testWorkflowValidationMultipleState() - { - $this->createContainerFromClosure(function ($container) { + $this->fail('No LogicException thrown'); + } catch (LogicException $e) { + $this->assertEquals('Rate limiter "with_lock" requires the Lock component to be installed and configured.', $e->getMessage()); + } + + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { $container->loadFromExtension('framework', [ - 'workflows' => [ - 'article' => [ - 'type' => 'workflow', - 'marking_store' => [ - 'type' => 'multiple_state', - ], - 'supports' => [ - __CLASS__, - ], - 'places' => [ - 'a', - 'b', - 'c', - ], - 'transitions' => [ - 'a_to_b' => [ - 'from' => ['a'], - 'to' => ['b', 'c'], - ], - ], - ], + 'lock' => true, + 'rate_limiter' => [ + 'with_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], ], ]); }); - // the test ensures that the validation does not fail (i.e. it does not throw any exceptions) - $this->addToAssertionCount(1); + $withLock = $container->getDefinition('limiter.with_lock'); + $this->assertEquals('lock.factory', (string) $withLock->getArgument(2)); } - /** - * @group legacy - */ - public function testWorkflowValidationSingleState() + public function testRateLimiterLockFactory() { - $this->expectException(InvalidDefinitionException::class); - $this->expectExceptionMessage('The marking store of workflow "article" can not store many places. But the transition "a_to_b" has too many output (2). Only one is accepted.'); - $this->createContainerFromClosure(function ($container) { + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { $container->loadFromExtension('framework', [ - 'workflows' => [ - 'article' => [ - 'type' => 'workflow', - 'marking_store' => [ - 'type' => 'single_state', - ], - 'supports' => [ - __CLASS__, - ], - 'places' => [ - 'a', - 'b', - 'c', - ], - 'transitions' => [ - 'a_to_b' => [ - 'from' => ['a'], - 'to' => ['b', 'c'], - ], - ], - ], + 'rate_limiter' => [ + 'without_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour', 'lock_factory' => null], ], ]); }); - // the test ensures that the validation does not fail (i.e. it does not throw any exceptions) - $this->addToAssertionCount(1); + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessageMatches('/^The argument "2" doesn\'t exist.*\.$/'); + + $container->getDefinition('limiter.without_lock')->getArgument(2); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php deleted file mode 100644 index 2aa519acfb2b1..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\EventListener; - -use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; -use Symfony\Bundle\FrameworkBundle\EventListener\ResolveControllerNameSubscriber; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -/** - * @group legacy - */ -class ResolveControllerNameSubscriberTest extends TestCase -{ - public function testReplacesControllerAttribute() - { - $parser = $this->createMock(ControllerNameParser::class); - $parser->expects($this->any()) - ->method('parse') - ->with('AppBundle:Starting:format') - ->willReturn('App\\Final\\Format::methodName'); - $httpKernel = $this->createMock(HttpKernelInterface::class); - - $request = new Request(); - $request->attributes->set('_controller', 'AppBundle:Starting:format'); - - $subscriber = new ResolveControllerNameSubscriber($parser); - $subscriber->onKernelRequest(new RequestEvent($httpKernel, $request, HttpKernelInterface::MASTER_REQUEST)); - $this->assertEquals('App\\Final\\Format::methodName', $request->attributes->get('_controller')); - - $subscriber = new ChildResolveControllerNameSubscriber($parser); - $subscriber->onKernelRequest(new RequestEvent($httpKernel, $request, HttpKernelInterface::MASTER_REQUEST)); - $this->assertEquals('App\\Final\\Format::methodName', $request->attributes->get('_controller')); - } - - /** - * @dataProvider provideSkippedControllers - */ - public function testSkipsOtherControllerFormats($controller) - { - $parser = $this->createMock(ControllerNameParser::class); - $parser->expects($this->never()) - ->method('parse'); - $httpKernel = $this->createMock(HttpKernelInterface::class); - - $request = new Request(); - $request->attributes->set('_controller', $controller); - - $subscriber = new ResolveControllerNameSubscriber($parser); - $subscriber->onKernelRequest(new RequestEvent($httpKernel, $request, HttpKernelInterface::MASTER_REQUEST)); - $this->assertEquals($controller, $request->attributes->get('_controller')); - } - - public function provideSkippedControllers() - { - yield ['Other:format']; - yield [function () {}]; - } -} - -class ChildResolveControllerNameSubscriber extends ResolveControllerNameSubscriber -{ - public function onKernelRequest(GetResponseEvent $event) - { - parent::onKernelRequest($event); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/BaseBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/BaseBundle.php deleted file mode 100644 index 494a18dff0a14..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/BaseBundle/BaseBundle.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BaseBundle; - -use Symfony\Component\HttpKernel\Bundle\Bundle; - -class BaseBundle extends Bundle -{ -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.json index e07b137438464..8835a17d6f46e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.json +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.json @@ -60,7 +60,11 @@ "type": "service", "id": ".definition_2" } - ] + ], + { + "type": "abstract", + "text": "placeholder" + } ], "file": null, "factory_class": "Full\\Qualified\\FactoryClass", diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.md index 08676b31d9b2c..66d57cd84c340 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.md +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.md @@ -51,4 +51,3 @@ Aliases - Service: `service_1` - Public: yes - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.xml index 061bb5d6ef219..3a51fa1646908 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_arguments.xml @@ -22,6 +22,7 @@ + placeholder diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md index be23f839bf474..b7daad45a8ed3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_public.md @@ -48,4 +48,3 @@ Aliases - Service: `service_1` - Public: yes - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md index 2d0edfd01952e..d793c5900a65a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.md @@ -33,4 +33,3 @@ Aliases - Service: `.service_2` - Public: no - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt index 82b4909242d84..cdefb65d208dd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_services.txt @@ -7,5 +7,5 @@ --------------- ------------------------ .alias_2 alias for ".service_2" .definition_2 Full\Qualified\Class2 - --------------- ------------------------ + --------------- ------------------------ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt index 33c96b30d8ae3..a6315ed20c420 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_1_tag1.txt @@ -2,10 +2,10 @@ Symfony Container Hidden Services Tagged with "tag1" Tag ======================================================== - --------------- ------- ------- ------- ----------------------- -  Service ID   attr1   attr2   attr3   Class name  - --------------- ------- ------- ------- ----------------------- - .definition_2 val1 val2 Full\Qualified\Class2 - " val3 - --------------- ------- ------- ------- ----------------------- + ------------------------------------------ ------- ------- ------- ----------------------- +  Service ID   attr1   attr2   attr3   Class name  + ------------------------------------------ ------- ------- ------- ----------------------- + .definition_2 val1 val2 Full\Qualified\Class2 + (same service as previous, another tag) val3 + ------------------------------------------ ------- ------- ------- ----------------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt index 7884a05c2a690..b6439a7b31768 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt @@ -2,14 +2,14 @@ Symfony Container Services Tagged with "tag1" Tag ================================================= - -------------- ------- ------- ---------- ------- ----------------------- -  Service ID   attr1   attr2   priority   attr3   Class name  - -------------- ------- ------- ---------- ------- ----------------------- - definition_3 40 val3 Full\Qualified\Class3 - " val1 val2 0 - definition_1 val1 30 Full\Qualified\Class1 - " val2 - definition_4 0 Full\Qualified\Class4 - definition_2 val1 val2 -20 Full\Qualified\Class2 - -------------- ------- ------- ---------- ------- ----------------------- + ------------------------------------------ ------- ------- ---------- ------- ----------------------- +  Service ID   attr1   attr2   priority   attr3   Class name  + ------------------------------------------ ------- ------- ---------- ------- ----------------------- + definition_3 40 val3 Full\Qualified\Class3 + (same service as previous, another tag) val1 val2 0 + definition_1 val1 30 Full\Qualified\Class1 + (same service as previous, another tag) val2 + definition_4 0 Full\Qualified\Class4 + definition_2 val1 val2 -20 Full\Qualified\Class2 + ------------------------------------------ ------- ------- ---------- ------- ----------------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/cache/KernelContainerWithDeprecations.log b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/cache/KernelContainerWithDeprecations.log new file mode 100644 index 0000000000000..42d6eb81be680 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/cache/KernelContainerWithDeprecations.log @@ -0,0 +1 @@ +a:2:{i:0;a:6:{s:4:"type";i:16384;s:7:"message";s:25:"Some deprecation message.";s:4:"file";s:22:"/path/to/some/file.php";s:4:"line";i:39;s:5:"trace";a:0:{}s:5:"count";i:3;}i:1;a:6:{s:4:"type";i:16384;s:7:"message";s:29:"An other deprecation message.";s:4:"file";s:26:"/path/to/an/other/file.php";s:4:"line";i:25;s:5:"trace";a:0:{}s:5:"count";i:2;}} \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/cache/KernelContainerWithoutDeprecations.log b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/cache/KernelContainerWithoutDeprecations.log new file mode 100644 index 0000000000000..c856afcf97010 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/cache/KernelContainerWithoutDeprecations.log @@ -0,0 +1 @@ +a:0:{} \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.json index 209557d5baf5d..769679c6d23f5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.json +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.json @@ -58,7 +58,11 @@ "type": "service", "id": ".definition_2" } - ] + ], + { + "type": "abstract", + "text": "placeholder" + } ], "file": null, "factory_class": "Full\\Qualified\\FactoryClass", diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt index 2e9dc9771c09d..af3410f83d5b9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt @@ -1,24 +1,25 @@ - ---------------- ----------------------------- -  Option   Value  - ---------------- ----------------------------- - Service ID - - Class Full\Qualified\Class1 - Tags - - Public yes - Synthetic no - Lazy yes - Shared yes - Abstract yes - Autowired no - Autoconfigured no - Factory Class Full\Qualified\FactoryClass - Factory Method get - Arguments Service(.definition_2)  - %parameter%  - Inlined Service  - Array (3 element(s))  - Iterator (2 element(s))  - - Service(definition_1)  - - Service(.definition_2) - ---------------- ----------------------------- + ---------------- --------------------------------- +  Option   Value  + ---------------- --------------------------------- + Service ID - + Class Full\Qualified\Class1 + Tags - + Public yes + Synthetic no + Lazy yes + Shared yes + Abstract yes + Autowired no + Autoconfigured no + Factory Class Full\Qualified\FactoryClass + Factory Method get + Arguments Service(.definition_2)  + %parameter%  + Inlined Service  + Array (3 element(s))  + Iterator (2 element(s))  + - Service(definition_1)  + - Service(.definition_2)  + Abstract argument (placeholder) + ---------------- --------------------------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.xml index c3a099c152780..b71fe8e144b68 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.xml @@ -20,4 +20,5 @@ + placeholder diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.json new file mode 100644 index 0000000000000..8cd2a441cb474 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.json @@ -0,0 +1,17 @@ +{ + "remainingCount": 5, + "deprecations": [ + { + "message": "Some deprecation message.", + "file": "\/path\/to\/some\/file.php", + "line": 39, + "count": 3 + }, + { + "message": "An other deprecation message.", + "file": "\/path\/to\/an\/other\/file.php", + "line": 25, + "count": 2 + } + ] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.md new file mode 100644 index 0000000000000..c6f7f31a92f9c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.md @@ -0,0 +1,4 @@ +## Remaining deprecations (5) + +- 3x: "Some deprecation message." in /path/to/some/file.php:39 +- 2x: "An other deprecation message." in /path/to/an/other/file.php:25 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.txt new file mode 100644 index 0000000000000..b869cb834b28d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.txt @@ -0,0 +1,9 @@ + +Remaining deprecations (5) +========================== + + * 3x: Some deprecation message. + in /path/to/some/file.php:39 + * 2x: An other deprecation message. + in /path/to/an/other/file.php:25 + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.xml new file mode 100644 index 0000000000000..bd4bd006317a0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations.xml @@ -0,0 +1,13 @@ + + + + Some deprecation message. + /path/to/some/file.php + 39 + + + An other deprecation message. + /path/to/an/other/file.php + 25 + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.json new file mode 100644 index 0000000000000..15b98cce27dcc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.json @@ -0,0 +1,4 @@ +{ + "remainingCount": 0, + "deprecations": [] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.md new file mode 100644 index 0000000000000..eb5f567c6ddee --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.md @@ -0,0 +1 @@ +## There are no deprecations in the logs! diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.txt new file mode 100644 index 0000000000000..43a62a9e71067 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.txt @@ -0,0 +1,5 @@ + +  + [OK] There are no deprecations in the logs!  +  + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.xml new file mode 100644 index 0000000000000..f469736ac42aa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/deprecations_empty.xml @@ -0,0 +1,2 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt new file mode 100644 index 0000000000000..4d4a18e5a71b8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt @@ -0,0 +1,18 @@ ++--------------+-----------------------------------------------------------------------------------------------+ +| Property | Value | ++--------------+-----------------------------------------------------------------------------------------------+ +| Route Name | | +| Path | /hello/{name} | +| Path Regex | #PATH_REGEX# | +| Host | localhost | +| Host Regex | #HOST_REGEX# | +| Scheme | http|https | +| Method | GET|HEAD | +| Requirements | name: [a-z]+ | +| Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | +| | name: Joseph | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | ++--------------+-----------------------------------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt new file mode 100644 index 0000000000000..a690b9798d90a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt @@ -0,0 +1,18 @@ ++--------------+-----------------------------------------------------------------------------------------------+ +| Property | Value | ++--------------+-----------------------------------------------------------------------------------------------+ +| Route Name | | +| Path | /name/add | +| Path Regex | #PATH_REGEX# | +| Host | localhost | +| Host Regex | #HOST_REGEX# | +| Scheme | http|https | +| Method | PUT|POST | +| Requirements | NO CUSTOM | +| Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | +| Condition | context.getMethod() in ['GET', 'HEAD', 'POST'] | ++--------------+-----------------------------------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php index b8a20d246a096..7b36fba5d932b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php @@ -26,12 +26,6 @@ trans('single-quoted key with "quote mark at the end"') ?> -transChoice( - '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples', - 10, - ['%count%' => 10] -) ?> - trans('other-domain-test-no-params-short-array', [], 'not_messages'); ?> trans('other-domain-test-no-params-long-array', [], 'not_messages'); ?> @@ -40,10 +34,4 @@ trans('other-domain-test-params-long-array', ['foo' => 'bar'], 'not_messages'); ?> -transChoice('other-domain-test-trans-choice-short-array-%count%', 10, ['%count%' => 10], 'not_messages'); ?> - -transChoice('other-domain-test-trans-choice-long-array-%count%', 10, ['%count%' => 10], 'not_messages'); ?> - trans('typecast', ['a' => (int) '123'], 'not_messages'); ?> -transChoice('msg1', 10 + 1, [], 'not_messages'); ?> -transChoice('msg2', ceil(4.5), [], 'not_messages'); ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TokenInterface.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TokenInterface.php deleted file mode 100644 index 4de75ac5b3f35..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TokenInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -assertSame(200, $client->getResponse()->getStatusCode()); $this->assertSame($expectedValue, $client->getResponse()->getContent()); + + $router = self::getContainer()->get('router'); + + $this->assertSame('/annotated/create-transaction', $router->generate('symfony_framework_tests_functional_test_annotated_createtransaction')); } public function getRoutes() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php index fdeaf98fb0293..e60bb93ea22a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php @@ -12,14 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\PsrCachedReader; -use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface as FrameworkBundleEngineInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Templating\EngineInterface as ComponentEngineInterface; class AutowiringTypesTest extends AbstractWebTestCase { @@ -27,7 +24,7 @@ public function testAnnotationReaderAutowiring() { static::bootKernel(['root_config' => 'no_annotations_cache.yml', 'environment' => 'no_annotations_cache']); - $annotationReader = static::$container->get('test.autowiring_types.autowired_services')->getAnnotationReader(); + $annotationReader = self::getContainer()->get('test.autowiring_types.autowired_services')->getAnnotationReader(); $this->assertInstanceOf(AnnotationReader::class, $annotationReader); } @@ -35,36 +32,20 @@ public function testCachedAnnotationReaderAutowiring() { static::bootKernel(); - $annotationReader = static::$container->get('test.autowiring_types.autowired_services')->getAnnotationReader(); - if (class_exists(PsrCachedReader::class)) { - $this->assertInstanceOf(PsrCachedReader::class, $annotationReader); - } else { - $this->assertInstanceOf(CachedReader::class, $annotationReader); - } - } - - /** - * @group legacy - */ - public function testTemplatingAutowiring() - { - static::bootKernel(['root_config' => 'templating.yml', 'environment' => 'templating']); - - $autowiredServices = static::$container->get('test.autowiring_types.autowired_services'); - $this->assertInstanceOf(FrameworkBundleEngineInterface::class, $autowiredServices->getFrameworkBundleEngine()); - $this->assertInstanceOf(ComponentEngineInterface::class, $autowiredServices->getEngine()); + $annotationReader = self::getContainer()->get('test.autowiring_types.autowired_services')->getAnnotationReader(); + $this->assertInstanceOf(PsrCachedReader::class, $annotationReader); } public function testEventDispatcherAutowiring() { static::bootKernel(['debug' => false]); - $autowiredServices = static::$container->get('test.autowiring_types.autowired_services'); + $autowiredServices = self::getContainer()->get('test.autowiring_types.autowired_services'); $this->assertInstanceOf(EventDispatcher::class, $autowiredServices->getDispatcher(), 'The event_dispatcher service should be injected if the debug is not enabled'); static::bootKernel(['debug' => true]); - $autowiredServices = static::$container->get('test.autowiring_types.autowired_services'); + $autowiredServices = self::getContainer()->get('test.autowiring_types.autowired_services'); $this->assertInstanceOf(TraceableEventDispatcher::class, $autowiredServices->getDispatcher(), 'The debug.event_dispatcher service should be injected if the debug is enabled'); } @@ -72,7 +53,7 @@ public function testCacheAutowiring() { static::bootKernel(); - $autowiredServices = static::$container->get('test.autowiring_types.autowired_services'); + $autowiredServices = self::getContainer()->get('test.autowiring_types.autowired_services'); $this->assertInstanceOf(FilesystemAdapter::class, $autowiredServices->getCachePool()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php index 79f0a7006c89b..265e9bb138fbf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php @@ -16,7 +16,7 @@ public function getNamespace(): string return ''; } - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string|false { return false; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php deleted file mode 100644 index 7fc0cdd7b55af..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ -class TemplatingServices -{ - private $frameworkBundleEngine; - private $engine; - - public function __construct(FrameworkBundleEngineInterface $frameworkBundleEngine, EngineInterface $engine) - { - $this->frameworkBundleEngine = $frameworkBundleEngine; - $this->engine = $engine; - } - - public function getFrameworkBundleEngine() - { - return $this->frameworkBundleEngine; - } - - public function getEngine() - { - return $this->engine; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php index 96543ce10f6b4..f2f077786f2b7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/AnnotatedController.php @@ -48,4 +48,12 @@ public function argumentWithoutDefaultWithRouteParamAndDefaultAction($value) { return new Response($value); } + + /** + * @Route("/create-transaction") + */ + public function createTransaction() + { + return new Response(); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php index 8436a3b24809a..42044570201e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/FragmentController.php @@ -15,6 +15,8 @@ use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ControllerReference; +use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; use Twig\Environment; class FragmentController implements ContainerAwareInterface @@ -45,6 +47,11 @@ public function forwardLocaleAction(Request $request) { return new Response($request->getLocale()); } + + public function fragmentUriAction(Request $request, FragmentUriGeneratorInterface $fragmentUriGenerator) + { + return new Response($fragmentUriGenerator->generate(new ControllerReference(self::class.'::indexAction'), $request)); + } } class Bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/InjectedFlashbagSessionController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/InjectedFlashbagSessionController.php deleted file mode 100644 index 20c33a17e4353..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/InjectedFlashbagSessionController.php +++ /dev/null @@ -1,36 +0,0 @@ -flashBag = $flashBag; - $this->router = $router; - } - - public function setFlashAction(Request $request, $message) - { - $this->flashBag->add('notice', $message); - - return new RedirectResponse($this->router->generate('session_showflash')); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SecurityController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SecurityController.php new file mode 100644 index 0000000000000..d90d7512f24aa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SecurityController.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; + +class SecurityController implements ServiceSubscriberInterface +{ + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public function profileAction() + { + return new Response('Welcome '.$this->container->get('security.token_storage')->getToken()->getUserIdentifier().'!'); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices(): array + { + return [ + 'security.token_storage' => TokenStorageInterface::class, + ]; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php deleted file mode 100644 index b8b53c25044cd..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php +++ /dev/null @@ -1,21 +0,0 @@ -hasDefinition('console.command.translation_debug')) { - // skipping the /Resources/views path deprecation - $container->getDefinition('console.command.translation_debug') - ->setArgument(4, '%kernel.project_dir%/Resources/views'); - } - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml index 155871fc278ec..8d41ce3267131 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml @@ -18,10 +18,6 @@ session_setflash: path: /session_setflash/{message} defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::setFlashAction } -injected_flashbag_session_setflash: - path: injected_flashbag/session_setflash/{message} - defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController::setFlashAction} - session_showflash: path: /session_showflash defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::showFlashAction } @@ -53,6 +49,10 @@ fragment_inlined: path: /fragment_inlined defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::inlinedAction } +fragment_uri: + path: /fragment_uri + defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\FragmentController::fragmentUriAction } + array_controller: path: /array_controller defaults: { _controller: [ArrayController, someAction] } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Slugger/SlugConstructArgService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Slugger/SlugConstructArgService.php new file mode 100644 index 0000000000000..943fda4b6b9e0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Slugger/SlugConstructArgService.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger; + +use Symfony\Component\String\Slugger\SluggerInterface; + +class SlugConstructArgService +{ + private $slugger; + + public function __construct(SluggerInterface $slugger) + { + $this->slugger = $slugger; + } + + public function hello(): string + { + return $this->slugger->slug('Стойността трябва да бъде лъжа'); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php index db4b2504aa3ec..13c368fd585e4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -13,9 +13,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\AnnotationReaderPass; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config\CustomConfig; -use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\TranslationDebugPass; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; @@ -39,16 +37,6 @@ public function build(ContainerBuilder $container) $extension->setCustomConfig(new CustomConfig()); $container->addCompilerPass(new AnnotationReaderPass(), PassConfig::TYPE_AFTER_REMOVING); - $container->addCompilerPass(new TranslationDebugPass()); - - $container->addCompilerPass(new class() implements CompilerPassInterface { - public function process(ContainerBuilder $container) - { - $container->removeDefinition('twig.controller.exception'); - $container->removeDefinition('twig.controller.preview_error'); - } - }); - $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php index f447300c2c69c..a079837c9e336 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php @@ -40,8 +40,8 @@ public function testBundlePublicDir() public function testBundleTwigTemplatesDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $twig = static::$container->get('twig'); - $bundlesMetadata = static::$container->getParameter('kernel.bundles_metadata'); + $twig = static::getContainer()->get('twig.alias'); + $bundlesMetadata = static::getContainer()->getParameter('kernel.bundles_metadata'); $this->assertSame([$bundlesMetadata['LegacyBundle']['path'].'/Resources/views'], $twig->getLoader()->getPaths('Legacy')); $this->assertSame("OK\n", $twig->render('@Legacy/index.html.twig')); @@ -53,7 +53,7 @@ public function testBundleTwigTemplatesDir() public function testBundleTranslationsDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $translator = static::$container->get('translator'); + $translator = static::getContainer()->get('translator.alias'); $this->assertSame('OK', $translator->trans('ok_label', [], 'legacy')); $this->assertSame('OK', $translator->trans('ok_label', [], 'modern')); @@ -62,7 +62,7 @@ public function testBundleTranslationsDir() public function testBundleValidationConfigDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $validator = static::$container->get('validator'); + $validator = static::getContainer()->get('validator.alias'); $this->assertTrue($validator->hasMetadataFor(LegacyPerson::class)); $this->assertCount(1, $constraintViolationList = $validator->validate(new LegacyPerson('john', 5))); @@ -76,7 +76,7 @@ public function testBundleValidationConfigDir() public function testBundleSerializationConfigDir() { static::bootKernel(['test_case' => 'BundlePaths']); - $serializer = static::$container->get('serializer'); + $serializer = static::getContainer()->get('serializer.alias'); $this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new LegacyPerson('john', 5), 'json')); $this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new ModernPerson('john', 5), 'json')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php index a3a0b23136137..b3885edaa3f36 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php @@ -13,8 +13,10 @@ use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Symfony\Component\Finder\SplFileInfo; /** * @group functional @@ -31,7 +33,7 @@ public function testClearPrivatePool() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.private_pool']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Clearing cache pool: cache.private_pool', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -41,7 +43,7 @@ public function testClearPublicPool() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.public_pool']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Clearing cache pool: cache.public_pool', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -51,7 +53,7 @@ public function testClearPoolWithCustomClearer() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.pool_with_clearer']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Clearing cache pool: cache.pool_with_clearer', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -61,7 +63,7 @@ public function testCallClearer() $tester = $this->createCommandTester(); $tester->execute(['pools' => ['cache.app_clearer']], ['decorated' => false]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:clear exits with 0 in case of success'); $this->assertStringContainsString('Calling cache clearer: cache.app_clearer', $tester->getDisplay()); $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } @@ -74,10 +76,39 @@ public function testClearUnexistingPool() ->execute(['pools' => ['unknown_pool']], ['decorated' => false]); } + public function testClearFailed() + { + $tester = $this->createCommandTester(); + /** @var FilesystemAdapter $pool */ + $pool = static::getContainer()->get('cache.public_pool'); + $item = $pool->getItem('foo'); + $item->set('baz'); + $pool->save($item); + $r = new \ReflectionObject($pool); + $p = $r->getProperty('directory'); + $p->setAccessible(true); + $poolDir = $p->getValue($pool); + + /** @var SplFileInfo $entry */ + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($poolDir)) as $entry) { + // converts files into dir to make adapter fail + if ($entry->isFile()) { + unlink($entry->getPathname()); + mkdir($entry->getPathname()); + } + } + + $tester->execute(['pools' => ['cache.public_pool']]); + + $this->assertSame(1, $tester->getStatusCode(), 'cache:pool:clear exits with 1 in case of error'); + $this->assertStringNotContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); + $this->assertStringContainsString('[WARNING] Cache pool "cache.public_pool" could not be cleared.', $tester->getDisplay()); + } + private function createCommandTester() { $application = new Application(static::$kernel); - $application->add(new CachePoolClearCommand(static::$container->get('cache.global_clearer'))); + $application->add(new CachePoolClearCommand(static::getContainer()->get('cache.global_clearer'))); return new CommandTester($application->find('cache:pool:clear')); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php index d7e5aae80c4f8..8e9061845a45e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php @@ -30,7 +30,7 @@ public function testListPools() $tester = $this->createCommandTester(['cache.app', 'cache.system']); $tester->execute([]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:list exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:list exits with 0 in case of success'); $this->assertStringContainsString('cache.app', $tester->getDisplay()); $this->assertStringContainsString('cache.system', $tester->getDisplay()); } @@ -40,7 +40,7 @@ public function testEmptyList() $tester = $this->createCommandTester([]); $tester->execute([]); - $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:list exits with 0 in case of success'); + $tester->assertCommandIsSuccessful('cache:pool:list exits with 0 in case of success'); } private function createCommandTester(array $poolNames) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php index fc53e26661993..2bebd2d77a3e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php @@ -68,7 +68,7 @@ public function testRedisCustomCachePools() private function doTestCachePools($options, $adapterClass) { static::bootKernel($options); - $container = static::$container; + $container = static::getContainer(); $pool1 = $container->get('cache.pool1'); $this->assertInstanceOf($adapterClass, $pool1); @@ -86,7 +86,7 @@ private function doTestCachePools($options, $adapterClass) $pool2 = $container->get('cache.pool2'); $pool2->save($item); - $container->get('cache_clearer')->clear($container->getParameter('kernel.cache_dir')); + $container->get('cache_clearer.alias')->clear($container->getParameter('kernel.cache_dir')); $item = $pool1->getItem($key); $this->assertFalse($item->isHit()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php index 0df853997c59a..8135f4dcfe419 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -111,6 +113,31 @@ public function testDumpThrowsExceptionWhenDefaultConfigFallbackIsImpossible() $tester->execute(['name' => 'ExtensionWithoutConfigTestBundle']); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $this->application->add(new ConfigDebugCommand()); + + $tester = new CommandCompletionTester($this->application->get('debug:config')); + + $suggestions = $tester->complete($input); + + foreach ($expectedSuggestions as $expectedSuggestion) { + $this->assertContains($expectedSuggestion, $suggestions); + } + } + + public function provideCompletionSuggestions(): \Generator + { + yield 'name' => [[''], ['default_config_test', 'extension_without_config_test', 'framework', 'test']]; + + yield 'name (started CamelCase)' => [['Fra'], ['DefaultConfigTestBundle', 'ExtensionWithoutConfigTestBundle', 'FrameworkBundle', 'TestBundle']]; + + yield 'name with existing path' => [['framework', ''], ['secret', 'router.resource', 'router.utf8', 'router.enabled', 'validation.enabled', 'default_locale']]; + } + private function createCommandTester(): CommandTester { $command = $this->application->find('debug:config'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php index f4298ac9a851c..e86a7ff790ef7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\ConfigDumpReferenceCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -30,6 +32,14 @@ protected function setUp(): void $this->application->doRun(new ArrayInput([]), new NullOutput()); } + public function testDumpKernelExtension() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'foo']); + $this->assertStringContainsString('foo:', $tester->getDisplay()); + $this->assertStringContainsString(' bar', $tester->getDisplay()); + } + public function testDumpBundleName() { $tester = $this->createCommandTester(); @@ -73,6 +83,23 @@ public function testDumpAtPathXml() $this->assertStringContainsString('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $this->application->add(new ConfigDumpReferenceCommand()); + $tester = new CommandCompletionTester($this->application->get('config:dump-reference')); + $suggestions = $tester->complete($input, 2); + $this->assertSame($expectedSuggestions, $suggestions); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'name' => [[''], ['DefaultConfigTestBundle', 'default_config_test', 'ExtensionWithoutConfigTestBundle', 'extension_without_config_test', 'FrameworkBundle', 'framework', 'TestBundle', 'test']]; + yield 'option --format' => [['--format', ''], ['yaml', 'xml']]; + } + private function createCommandTester(): CommandTester { $command = $this->application->find('config:dump-reference'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php index 537ee77174622..ff2cc045b7ac1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Tester\CommandCompletionTester; /** * @group functional @@ -27,12 +28,12 @@ public function testDumpContainerIfNotExists() $application = new Application(static::$kernel); $application->setAutoExit(false); - @unlink(static::$container->getParameter('debug.container.dump')); + @unlink(static::getContainer()->getParameter('debug.container.dump')); $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container']); - $this->assertFileExists(static::$container->getParameter('debug.container.dump')); + $this->assertFileExists(static::getContainer()->getParameter('debug.container.dump')); } public function testNoDebug() @@ -91,7 +92,7 @@ public function testDescribeEnvVars() $application = new Application(static::$kernel); $application->setAutoExit(false); - @unlink(static::$container->getParameter('debug.container.dump')); + @unlink(static::getContainer()->getParameter('debug.container.dump')); $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--env-vars' => true], ['decorated' => false]); @@ -128,7 +129,7 @@ public function testDescribeEnvVar() $application = new Application(static::$kernel); $application->setAutoExit(false); - @unlink(static::$container->getParameter('debug.container.dump')); + @unlink(static::getContainer()->getParameter('debug.container.dump')); $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--env-var' => 'js'], ['decorated' => false]); @@ -136,6 +137,73 @@ public function testDescribeEnvVar() $this->assertStringContainsString(file_get_contents(__DIR__.'/Fixtures/describe_env_vars.txt'), $tester->getDisplay(true)); } + public function testGetDeprecation() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); + $path = sprintf('%s/%sDeprecations.log', static::$kernel->getContainer()->getParameter('kernel.build_dir'), static::$kernel->getContainer()->getParameter('kernel.container_class')); + 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, + ]])); + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + @unlink(static::getContainer()->getParameter('debug.container.dump')); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:container', '--deprecations' => true]); + + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Controller\Controller', $tester->getDisplay()); + $this->assertStringContainsString('/home/hamza/projet/contrib/sf/vendor/symfony/framework-bundle/Controller/Controller.php', $tester->getDisplay()); + } + + public function testGetDeprecationNone() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); + $path = sprintf('%s/%sDeprecations.log', static::$kernel->getContainer()->getParameter('kernel.build_dir'), static::$kernel->getContainer()->getParameter('kernel.container_class')); + touch($path); + file_put_contents($path, serialize([])); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + @unlink(static::getContainer()->getParameter('debug.container.dump')); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:container', '--deprecations' => true]); + + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('[OK] There are no deprecations in the logs!', $tester->getDisplay()); + } + + public function testGetDeprecationNoFile() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); + $path = sprintf('%s/%sDeprecations.log', static::$kernel->getContainer()->getParameter('kernel.build_dir'), static::$kernel->getContainer()->getParameter('kernel.container_class')); + @unlink($path); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + @unlink(static::getContainer()->getParameter('debug.container.dump')); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:container', '--deprecations' => true]); + + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('[WARNING] The deprecation file does not exist', $tester->getDisplay()); + } + public function provideIgnoreBackslashWhenFindingService() { return [ @@ -144,4 +212,68 @@ public function provideIgnoreBackslashWhenFindingService() ['\\'.BackslashClass::class], ]; } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions, array $notExpectedSuggestions = []) + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); + + $application = new Application(static::$kernel); + $tester = new CommandCompletionTester($application->find('debug:container')); + $suggestions = $tester->complete($input); + + foreach ($expectedSuggestions as $expectedSuggestion) { + $this->assertContains($expectedSuggestion, $suggestions); + } + foreach ($notExpectedSuggestions as $notExpectedSuggestion) { + $this->assertNotContains($notExpectedSuggestion, $suggestions); + } + } + + public function provideCompletionSuggestions() + { + $serviceId = 'console.command.container_debug'; + $hiddenServiceId = '.console.command.container_debug.lazy'; + $interfaceServiceId = 'Symfony\Component\HttpKernel\HttpKernelInterface'; + + yield 'name' => [ + [''], + [$serviceId, $interfaceServiceId], + [$hiddenServiceId], + ]; + + yield 'name (with hidden)' => [ + ['--show-hidden', ''], + [$serviceId, $interfaceServiceId, $hiddenServiceId], + ]; + + yield 'name (with current value)' => [ + ['--show-hidden', 'console'], + [$serviceId, $hiddenServiceId], + [$interfaceServiceId], + ]; + + yield 'name (no suggestion with --tags)' => [ + ['--tags', ''], + [], + [$serviceId, $interfaceServiceId, $hiddenServiceId], + ]; + + yield 'option --tag' => [ + ['--tag', ''], + ['console.command'], + ]; + + yield 'option --parameter' => [ + ['--parameter', ''], + ['kernel.debug'], + ]; + + yield 'option --format' => [ + ['--format', ''], + ['txt', 'xml', 'json', 'md'], + ]; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php index f543058440582..0546fd83179ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php @@ -20,13 +20,13 @@ public function testContainerCompilationInDebug() { $this->createClient(['test_case' => 'ContainerDump', 'root_config' => 'config.yml']); - $this->assertTrue(static::$container->has('serializer')); + $this->assertTrue(static::getContainer()->has('serializer')); } public function testContainerCompilation() { $this->createClient(['test_case' => 'ContainerDump', 'root_config' => 'config.yml', 'debug' => false]); - $this->assertTrue(static::$container->has('serializer')); + $this->assertTrue(static::getContainer()->has('serializer')); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php index a0ade821d5165..c3110cc71dcbb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Tester\CommandCompletionTester; /** * @group functional @@ -109,4 +111,26 @@ public function testNotConfusedByClassAliases() $tester->run(['command' => 'debug:autowiring', 'search' => 'ClassAlias']); $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ClassAliasExampleClass', $tester->getDisplay()); } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + $kernel = static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + $command = (new Application($kernel))->add(new DebugAutowiringCommand()); + + $tester = new CommandCompletionTester($command); + + $suggestions = $tester->complete($input); + + foreach ($expectedSuggestions as $expectedSuggestion) { + $this->assertContains($expectedSuggestion, $suggestions); + } + } + + public function provideCompletionSuggestions(): \Generator + { + yield 'search' => [[''], ['SessionHandlerInterface', 'Psr\\Log\\LoggerInterface', 'Psr\\Container\\ContainerInterface $parameterBag']]; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php index a4ac17238a4b8..b514ed3b8e042 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php @@ -44,4 +44,12 @@ public function getConfigs() [true], ]; } + + public function testGenerateFragmentUri() + { + $client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]); + $client->request('GET', '/fragment_uri'); + + $this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent()); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php new file mode 100644 index 0000000000000..32bee3b587309 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/KernelTestCaseTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Test\TestContainer; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\NonPublicService; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\PrivateService; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\PublicService; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\UnusedPrivateService; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class KernelTestCaseTest extends AbstractWebTestCase +{ + public function testThatPrivateServicesAreUnavailableIfTestConfigIsDisabled() + { + static::bootKernel(['test_case' => 'TestServiceContainer', 'root_config' => 'test_disabled.yml', 'environment' => 'test_disabled']); + + $this->expectException(\LogicException::class); + static::getContainer(); + } + + public function testThatPrivateServicesAreAvailableIfTestConfigIsEnabled() + { + static::bootKernel(['test_case' => 'TestServiceContainer']); + + $container = static::getContainer(); + $this->assertInstanceOf(ContainerInterface::class, $container); + $this->assertInstanceOf(TestContainer::class, $container); + $this->assertTrue($container->has(PublicService::class)); + $this->assertTrue($container->has(NonPublicService::class)); + $this->assertTrue($container->has(PrivateService::class)); + $this->assertTrue($container->has('private_service')); + $this->assertFalse($container->has(UnusedPrivateService::class)); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php index ec293315cafaa..ca371766deb22 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php @@ -28,8 +28,8 @@ public function testEnvelopeListener() $this->assertEquals('sender@example.org', $envelope->getSender()->getAddress()); }; - $eventDispatcher = self::$container->get(EventDispatcherInterface::class); - $logger = self::$container->get('logger'); + $eventDispatcher = self::getContainer()->get(EventDispatcherInterface::class); + $logger = self::getContainer()->get('logger'); $testTransport = new class($eventDispatcher, $logger, $onDoSend) extends AbstractTransport { /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php index 35c2e63b7e04a..7b65ca5c276e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php @@ -36,6 +36,27 @@ public function testProfilerIsDisabled($insulate) $this->assertNull($client->getProfile()); } + /** + * @dataProvider getConfigs + */ + public function testProfilerCollectParameter($insulate) + { + $client = $this->createClient(['test_case' => 'ProfilerCollectParameter', 'root_config' => 'config.yml']); + if ($insulate) { + $client->insulate(); + } + + $client->request('GET', '/profiler'); + $this->assertNull($client->getProfile()); + + // enable the profiler for the next request + $client->request('GET', '/profiler?profile=1'); + $this->assertIsObject($client->getProfile()); + + $client->request('GET', '/profiler'); + $this->assertNull($client->getProfile()); + } + public function getConfigs() { return [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php index d9821820c04e9..c61955d37bc20 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php @@ -19,7 +19,7 @@ public function testPhpDocPriority() { static::bootKernel(['test_case' => 'Serializer']); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))], static::$container->get('property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes')); + $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT))], static::getContainer()->get('property_info')->getTypes('Symfony\Bundle\FrameworkBundle\Tests\Functional\Dummy', 'codes')); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php index b7cf74798a232..e9a8cd143b802 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -69,6 +70,37 @@ public function testSearchWithThrow() $tester->execute(['name' => 'gerard'], ['interactive' => true]); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $tester = new CommandCompletionTester($this->application->get('debug:router')); + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions() + { + yield 'option --format' => [ + ['--format', ''], + ['txt', 'xml', 'json', 'md'], + ]; + + yield 'route_name' => [ + [''], + [ + 'routerdebug_session_welcome', + 'routerdebug_session_welcome_name', + 'routerdebug_session_logout', + 'routerdebug_test', + ], + ]; + } + private function createCommandTester(): CommandTester { $command = $this->application->get('debug:router'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SecurityTest.php new file mode 100644 index 0000000000000..d97039562119c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SecurityTest.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Component\Security\Core\User\InMemoryUser; + +class SecurityTest extends AbstractWebTestCase +{ + /** + * @dataProvider getUsers + */ + public function testLoginUser(string $username, array $roles, ?string $firewallContext) + { + $user = new InMemoryUser($username, 'the-password', $roles); + $client = $this->createClient(['test_case' => 'Security', 'root_config' => 'config.yml']); + + if (null === $firewallContext) { + $client->loginUser($user); + } else { + $client->loginUser($user, $firewallContext); + } + + $client->request('GET', '/'.($firewallContext ?? 'main').'/user_profile'); + $this->assertEquals('Welcome '.$username.'!', $client->getResponse()->getContent()); + } + + public function getUsers() + { + yield ['the-username', ['ROLE_FOO'], null]; + yield ['the-username', ['ROLE_FOO'], 'main']; + yield ['other-username', ['ROLE_FOO'], 'custom']; + yield ['stateless-username', ['ROLE_FOO'], 'stateless']; + + yield ['the-username', ['ROLE_FOO'], null]; + yield ['no-role-username', [], null]; + } + + public function testLoginUserMultipleRequests() + { + $user = new InMemoryUser('the-username', 'the-password', ['ROLE_FOO']); + $client = $this->createClient(['test_case' => 'Security', 'root_config' => 'config.yml']); + $client->loginUser($user); + + $client->request('GET', '/main/user_profile'); + $this->assertEquals('Welcome the-username!', $client->getResponse()->getContent()); + + $client->request('GET', '/main/user_profile'); + $this->assertEquals('Welcome the-username!', $client->getResponse()->getContent()); + } + + public function testLoginInBetweenRequests() + { + $user = new InMemoryUser('the-username', 'the-password', ['ROLE_FOO']); + $client = $this->createClient(['test_case' => 'Security', 'root_config' => 'config.yml']); + + $client->request('GET', '/main/user_profile'); + $this->assertTrue($client->getResponse()->isRedirect('http://localhost/login')); + + $client->loginUser($user); + + $client->request('GET', '/main/user_profile'); + $this->assertEquals('Welcome the-username!', $client->getResponse()->getContent()); + } + + public function testLoginUserMultipleTimes() + { + $userFoo = new InMemoryUser('the-username', 'the-password', ['ROLE_FOO']); + $userBar = new InMemoryUser('no-role-username', 'the-password'); + $client = $this->createClient(['test_case' => 'Security', 'root_config' => 'config.yml']); + $client->loginUser($userFoo); + + $client->request('GET', '/main/user_profile'); + $this->assertEquals('Welcome the-username!', $client->getResponse()->getContent()); + + $client->loginUser($userBar); + + $client->request('GET', '/main/user_profile'); + $this->assertEquals('Welcome no-role-username!', $client->getResponse()->getContent()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index b0d774bdd55e8..019aa418901d8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -20,7 +20,7 @@ public function testDeserializeArrayOfObject() { static::bootKernel(['test_case' => 'Serializer']); - $result = static::$container->get('serializer')->deserialize('{"bars": [{"id": 1}, {"id": 2}]}', Foo::class, 'json'); + $result = static::getContainer()->get('serializer.alias')->deserialize('{"bars": [{"id": 1}, {"id": 2}]}', Foo::class, 'json'); $bar1 = new Bar(); $bar1->id = 1; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php index 530492ab8b4ed..ae8c7afafd425 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php @@ -69,29 +69,6 @@ public function testFlash($config, $insulate) $this->assertStringContainsString('No flash was set.', $crawler->text()); } - /** - * Tests flash messages work when flashbag service is injected to the constructor. - * - * @dataProvider getConfigs - */ - public function testFlashOnInjectedFlashbag($config, $insulate) - { - $client = $this->createClient(['test_case' => 'Session', 'root_config' => $config]); - if ($insulate) { - $client->insulate(); - } - - // set flash - $client->request('GET', '/injected_flashbag/session_setflash/Hello%20world.'); - - // check flash displays on redirect - $this->assertStringContainsString('Hello world.', $client->followRedirect()->text()); - - // check flash is gone - $crawler = $client->request('GET', '/session_showflash'); - $this->assertStringContainsString('No flash was set.', $crawler->text()); - } - /** * See if two separate insulated clients can run without * polluting each other's session data. diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SluggerLocaleAwareTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SluggerLocaleAwareTest.php new file mode 100644 index 0000000000000..d8552977c0835 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SluggerLocaleAwareTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +/** + * @group functional + */ +class SluggerLocaleAwareTest extends AbstractWebTestCase +{ + /** + * @requires extension intl + */ + public function testLocalizedSlugger() + { + $kernel = static::createKernel(['test_case' => 'Slugger', 'root_config' => 'config.yml']); + $kernel->boot(); + + $service = $kernel->getContainer()->get('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService'); + + $this->assertSame('Stoinostta-tryabva-da-bude-luzha', $service->hello()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php index 4122a749dfd1d..76a373c0c05a9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php @@ -16,33 +16,28 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\PrivateService; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\PublicService; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\UnusedPrivateService; -use Symfony\Component\DependencyInjection\ContainerInterface; class TestServiceContainerTest extends AbstractWebTestCase { - public function testThatPrivateServicesAreUnavailableIfTestConfigIsDisabled() + public function testLogicExceptionIfTestConfigIsDisabled() { static::bootKernel(['test_case' => 'TestServiceContainer', 'root_config' => 'test_disabled.yml', 'environment' => 'test_disabled']); - $this->assertInstanceOf(ContainerInterface::class, static::$container); - $this->assertNotInstanceOf(TestContainer::class, static::$container); - $this->assertTrue(static::$container->has(PublicService::class)); - $this->assertFalse(static::$container->has(NonPublicService::class)); - $this->assertFalse(static::$container->has(PrivateService::class)); - $this->assertFalse(static::$container->has('private_service')); - $this->assertFalse(static::$container->has(UnusedPrivateService::class)); + $this->expectException(\LogicException::class); + + static::getContainer(); } public function testThatPrivateServicesAreAvailableIfTestConfigIsEnabled() { static::bootKernel(['test_case' => 'TestServiceContainer']); - $this->assertInstanceOf(TestContainer::class, static::$container); - $this->assertTrue(static::$container->has(PublicService::class)); - $this->assertTrue(static::$container->has(NonPublicService::class)); - $this->assertTrue(static::$container->has(PrivateService::class)); - $this->assertTrue(static::$container->has('private_service')); - $this->assertFalse(static::$container->has(UnusedPrivateService::class)); + $this->assertInstanceOf(TestContainer::class, static::getContainer()); + $this->assertTrue(static::getContainer()->has(PublicService::class)); + $this->assertTrue(static::getContainer()->has(NonPublicService::class)); + $this->assertTrue(static::getContainer()->has(PrivateService::class)); + $this->assertTrue(static::getContainer()->has('private_service')); + $this->assertFalse(static::getContainer()->has(UnusedPrivateService::class)); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php index 382c4b5d94731..70a6c923dc418 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -32,7 +33,11 @@ public function testDumpAllTrans() $tester = $this->createCommandTester(); $ret = $tester->execute(['locale' => 'en']); - $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertSame( + TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED, + $ret, + 'Returns appropriate exit code in the event of error' + ); $this->assertStringContainsString('missing messages hello_from_construct_arg_service', $tester->getDisplay()); $this->assertStringContainsString('missing messages hello_from_subscriber_service', $tester->getDisplay()); $this->assertStringContainsString('missing messages hello_from_property_service', $tester->getDisplay()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php index 94b9bfa012b3c..31fcc742fd05b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php @@ -12,9 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app; use Psr\Log\NullLogger; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -23,7 +25,7 @@ * * @author Johannes M. Schmitt */ -class AppKernel extends Kernel +class AppKernel extends Kernel implements ExtensionInterface, ConfigurationInterface { private $varDir; private $testCase; @@ -104,12 +106,31 @@ protected function getKernelParameters(): array return $parameters; } - public function getContainer(): ContainerInterface + public function getConfigTreeBuilder(): TreeBuilder { - if (!$this->container) { - throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); - } + $treeBuilder = new TreeBuilder('foo'); + $rootNode = $treeBuilder->getRootNode(); + $rootNode->children()->scalarNode('foo')->defaultValue('bar')->end()->end(); + + return $treeBuilder; + } - return parent::getContainer(); + public function load(array $configs, ContainerBuilder $container) + { + } + + public function getNamespace(): string + { + return ''; + } + + public function getXsdValidationBasePath(): string|false + { + return false; + } + + public function getAlias(): string + { + return 'foo'; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AutowiringTypes/templating.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AutowiringTypes/templating.yml deleted file mode 100644 index 765bfa9d70d48..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AutowiringTypes/templating.yml +++ /dev/null @@ -1,12 +0,0 @@ -imports: - - { resource: ../config/default.yml } - -services: - _defaults: { public: true } - test.autowiring_types.autowired_services: - class: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\AutowiringTypes\TemplatingServices - autowire: true - -framework: - templating: - engines: ['php'] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml index 3e1e53738cf93..82a5a5422ab7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml @@ -8,4 +8,19 @@ framework: twig: strict_variables: '%kernel.debug%' - exception_controller: null # to be removed in 5.0 + +services: + twig.alias: + alias: twig + + validator.alias: + alias: validator + public: true + + serializer.alias: + alias: serializer + public: true + + translator.alias: + alias: translator + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml index 8c7bcb4eb1fac..d7733e0e3217f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/config.yml @@ -1,5 +1,9 @@ imports: - - { resource: ../config/default.yml } + - { resource: default.yml } + +parameters: + env(LIFETIME_INTERVAL): 'PT10S' + env(LIFETIME_EXPRESSION): '13 seconds' framework: cache: @@ -7,9 +11,11 @@ framework: cache.pool1: public: true adapter: cache.system + default_lifetime: '%env(LIFETIME_EXPRESSION)%' cache.pool2: public: true adapter: cache.pool3 + default_lifetime: '%env(LIFETIME_INTERVAL)%' cache.pool3: clearer: ~ cache.pool4: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/default.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/default.yml new file mode 100644 index 0000000000000..c03efedd02bf7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/default.yml @@ -0,0 +1,7 @@ +imports: + - { resource: ../config/default.yml } + +services: + cache_clearer.alias: + alias: cache_clearer + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml index 30c69163d4f2f..e5ec8c4effbc4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ../config/default.yml } + - { resource: default.yml } parameters: env(REDIS_HOST): 'localhost' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml index 8681e59a7bb4a..419ee07a0aba0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ../config/default.yml } + - { resource: default.yml } parameters: env(REDIS_HOST): 'localhost' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml index a7a03a31d6602..6dba635a15555 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml @@ -4,7 +4,9 @@ imports: framework: secret: '%secret%' default_locale: '%env(LOCALE)%' + enabled_locales: ['%env(LOCALE)%'] session: + storage_factory_id: session.storage.factory.native cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%' parameters: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDump/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDump/config.yml index 5754ba969365b..be0eab4d5645e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDump/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDump/config.yml @@ -7,7 +7,8 @@ framework: fragments: true profiler: true router: true - session: true + session: + storage_factory_id: session.storage.factory.native request: true assets: true translator: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml index ceeea37a1001b..16fc81dd268d4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml @@ -7,4 +7,3 @@ framework: twig: strict_variables: '%kernel.debug%' - exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml index c2c3ace06f179..b464a41a0d857 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml @@ -9,3 +9,4 @@ framework: sender: sender@example.org recipients: - redirected@example.org + profiler: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/bundles.php new file mode 100644 index 0000000000000..15ff182c6fed5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; + +return [ + new FrameworkBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/config.yml new file mode 100644 index 0000000000000..67360dd321681 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/config.yml @@ -0,0 +1,8 @@ +imports: + - { resource: ../config/default.yml } + +framework: + profiler: + enabled: true + collect: false + collect_parameter: profile diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/routing.yml new file mode 100644 index 0000000000000..d4b77c3f703d9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ProfilerCollectParameter/routing.yml @@ -0,0 +1,2 @@ +_sessiontest_bundle: + resource: '@TestBundle/Resources/config/routing.yml' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/bundles.php new file mode 100644 index 0000000000000..bd57eef389b47 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/bundles.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/config.yml new file mode 100644 index 0000000000000..ff27b54bcf8d3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/config.yml @@ -0,0 +1,46 @@ +imports: + - { resource: ./../config/default.yml } + +services: + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SecurityController: + public: true + tags: + - container.service_subscriber + +security: + enable_authenticator_manager: true + + providers: + main: + memory: + users: + the-username: { password: the-password, roles: ['ROLE_FOO'] } + no-role-username: { password: the-password, roles: [] } + custom: + memory: + users: + other-username: { password: the-password, roles: ['ROLE_FOO'] } + stateless: + memory: + users: + stateless-username: { password: the-password, roles: ['ROLE_FOO'] } + + firewalls: + main: + pattern: ^/main + form_login: + check_path: /main/login/check + provider: main + custom: + pattern: ^/custom + form_login: + check_path: /custom/login/check + provider: custom + stateless: + pattern: ^/stateless + stateless: true + http_basic: ~ + provider: stateless + + access_control: + - { path: '^/main/user_profile$', roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/routing.yml new file mode 100644 index 0000000000000..0e8a27a44dd80 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Security/routing.yml @@ -0,0 +1,11 @@ +security_profile: + path: /main/user_profile + defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SecurityController::profileAction } + +security_custom_profile: + path: /custom/user_profile + defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SecurityController::profileAction } + +security_stateless_profile: + path: /stateless/user_profile + defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SecurityController::profileAction } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml index cac135c315d00..3721de1cac584 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Serializer/config.yml @@ -2,5 +2,13 @@ imports: - { resource: ../config/default.yml } framework: - serializer: { enabled: true } + serializer: + enabled: true + default_context: + enable_max_depth: true property_info: { enabled: true } + +services: + serializer.alias: + alias: serializer + public: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml index 4807c42d1ede8..ad6bdb691ca52 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml @@ -5,7 +5,3 @@ services: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SubRequestController: tags: - { name: controller.service_arguments, action: indexAction, argument: handler, id: fragment.handler } - - Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController: - autowire: true - tags: ['controller.service_arguments'] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/bundles.php new file mode 100644 index 0000000000000..15ff182c6fed5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; + +return [ + new FrameworkBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml new file mode 100644 index 0000000000000..669edf5667611 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml @@ -0,0 +1,15 @@ +imports: + - { resource: ../config/default.yml } + - { resource: services.yml } + +framework: + secret: '%secret%' + default_locale: '%env(LOCALE)%' + enabled_locales: ['%env(LOCALE)%'] + translator: + fallbacks: + - '%env(LOCALE)%' + +parameters: + env(LOCALE): bg + secret: test diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/services.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/services.yml new file mode 100644 index 0000000000000..b446d60a13b57 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/services.yml @@ -0,0 +1,6 @@ +services: + _defaults: + public: true + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService: + arguments: ['@slugger'] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml index 7f8815b2942fa..1cd6417b937b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml @@ -5,6 +5,7 @@ imports: framework: secret: '%secret%' default_locale: '%env(LOCALE)%' + enabled_locales: ['%env(LOCALE)%'] translator: fallbacks: - '%env(LOCALE)%' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml index 040708e011a60..c4087e32f585f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml @@ -1,13 +1,15 @@ framework: secret: test - router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } validation: { enabled: true, enable_annotations: true } csrf_protection: true - form: true + form: + enabled: true test: true default_locale: en + enabled_locales: ['en', 'fr'] session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file services: logger: { class: Psr\Log\NullLogger } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php index 6eb02fa11a8d9..d9dd700efd92e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php @@ -17,12 +17,11 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Routing\RouteCollectionBuilder; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface { @@ -33,7 +32,7 @@ class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface public function onKernelException(ExceptionEvent $event) { if ($event->getThrowable() instanceof Danger) { - $event->setResponse(Response::create('It\'s dangerous to go alone. Take this ⚔')); + $event->setResponse(new Response('It\'s dangerous to go alone. Take this ⚔')); } } @@ -74,25 +73,18 @@ public function __wakeup() throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } - public function __destruct() + protected function configureRoutes(RoutingConfigurator $routes): void { - if ($this->cacheDir) { - $fs = new Filesystem(); - $fs->remove($this->cacheDir); - } - } - - protected function configureRoutes(RouteCollectionBuilder $routes) - { - $routes->add('/', 'kernel::halloweenAction'); - $routes->add('/danger', 'kernel::dangerousAction'); + $routes->add('halloween', '/')->controller('kernel::halloweenAction'); + $routes->add('danger', '/danger')->controller('kernel::dangerousAction'); } - protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void { $c->register('logger', NullLogger::class); $c->loadFromExtension('framework', [ 'secret' => '$ecret', + 'router' => ['utf8' => true], ]); $c->setParameter('halloween', 'Have a great day!'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php index 4d7a11ea62e0e..d47ca5a822139 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php @@ -12,16 +12,39 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + +require_once __DIR__.'/flex-style/src/FlexStyleMicroKernel.php'; class MicroKernelTraitTest extends TestCase { + private $kernel; + + protected function tearDown(): void + { + if ($this->kernel) { + $kernel = $this->kernel; + $this->kernel = null; + $fs = new Filesystem(); + $fs->remove($kernel->getCacheDir()); + } + } + public function test() { - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->boot(); $request = Request::create('/'); @@ -34,7 +57,7 @@ public function test() public function testAsEventSubscriber() { - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->boot(); $request = Request::create('/danger'); @@ -52,8 +75,84 @@ public function testRoutingRouteLoaderTagIsAdded() ->willReturn('framework'); $container = new ContainerBuilder(); $container->registerExtension($frameworkExtension); - $kernel = new ConcreteMicroKernel('test', false); + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); $kernel->registerContainerConfiguration(new ClosureLoader($container)); $this->assertTrue($container->getDefinition('kernel')->hasTag('routing.route_loader')); } + + public function testFlexStyle() + { + $kernel = new FlexStyleMicroKernel('test', false); + $kernel->boot(); + + $request = Request::create('/'); + $response = $kernel->handle($request); + + $this->assertEquals('Have a great day!', $response->getContent()); + } + + public function testSecretLoadedFromExtension() + { + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); + $kernel->boot(); + + self::assertSame('$ecret', $kernel->getContainer()->getParameter('kernel.secret')); + } + + public function testAnonymousMicroKernel() + { + $kernel = new class('anonymous_kernel') extends MinimalKernel { + public function helloAction(): Response + { + return new Response('Hello World!'); + } + + protected function configureContainer(ContainerConfigurator $c): void + { + $c->extension('framework', [ + 'router' => ['utf8' => true], + ]); + $c->services()->set('logger', NullLogger::class); + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->add('hello', '/')->controller([$this, 'helloAction']); + } + }; + + $request = Request::create('/'); + $response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false); + + $this->assertSame('Hello World!', $response->getContent()); + } +} + +abstract class MinimalKernel extends Kernel +{ + use MicroKernelTrait; + + private $cacheDir; + + public function __construct(string $cacheDir) + { + parent::__construct('test', false); + + $this->cacheDir = sys_get_temp_dir().'/'.$cacheDir; + } + + public function registerBundles(): iterable + { + yield new FrameworkBundle(); + } + + public function getCacheDir(): string + { + return $this->cacheDir; + } + + public function getLogDir(): string + { + return $this->cacheDir; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php new file mode 100644 index 0000000000000..0691b2b32d19c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/config/bundles.php @@ -0,0 +1,7 @@ + ['all' => true], +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php new file mode 100644 index 0000000000000..d4f2ce121be54 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.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\Bundle\FrameworkBundle\Tests\Kernel; + +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + +class FlexStyleMicroKernel extends Kernel +{ + use MicroKernelTrait; + + private $cacheDir; + + public function halloweenAction(\stdClass $o) + { + return new Response($o->halloween); + } + + public function createHalloween(LoggerInterface $logger, string $halloween) + { + $o = new \stdClass(); + $o->logger = $logger; + $o->halloween = $halloween; + + return $o; + } + + public function getCacheDir(): string + { + return $this->cacheDir = sys_get_temp_dir().'/sf_flex_kernel'; + } + + public function getLogDir(): string + { + return $this->cacheDir; + } + + public function getProjectDir(): string + { + return \dirname((new \ReflectionObject($this))->getFileName(), 2); + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $fs = new Filesystem(); + $fs->remove($this->cacheDir); + } + + protected function configureRoutes(RoutingConfigurator $routes): void + { + $routes->add('halloween', '/')->controller([$this, 'halloweenAction']); + } + + protected function configureContainer(ContainerConfigurator $c): void + { + $c->parameters() + ->set('halloween', 'Have a great day!'); + + $c->services() + ->set('logger', NullLogger::class) + ->set('stdClass', 'stdClass') + ->autowire() + ->factory([$this, 'createHalloween']) + ->arg('$halloween', '%halloween%'); + + $c->extension('framework', ['router' => ['utf8' => true]]); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php index ce0f08b4e212d..f4bc970a5a84a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php @@ -3,7 +3,6 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderResolver; @@ -14,15 +13,10 @@ class DelegatingLoaderTest extends TestCase { - /** - * @group legacy - * @expectedDeprecation Passing a "Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser" instance as first argument to "Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader::__construct()" is deprecated since Symfony 4.4, pass a "Symfony\Component\Config\Loader\LoaderResolverInterface" instance instead. - */ public function testConstructorApi() { - $controllerNameParser = $this->createMock(ControllerNameParser::class); - new DelegatingLoader($controllerNameParser, new LoaderResolver()); - $this->assertTrue(true, '__construct() takes a ControllerNameParser and LoaderResolverInterface respectively as its first and second argument.'); + new DelegatingLoader(new LoaderResolver()); + $this->assertTrue(true, '__construct() takes a LoaderResolverInterface as its first argument.'); } public function testLoadDefaultOptions() @@ -37,13 +31,13 @@ public function testLoadDefaultOptions() $routeCollection = new RouteCollection(); $routeCollection->add('foo', new Route('/', [], [], ['utf8' => false])); - $routeCollection->add('bar', new Route('/', [], [], ['foo' => 123])); + $routeCollection->add('bar', new Route('/', [], ['_locale' => 'de'], ['foo' => 123])); $loader->expects($this->once()) ->method('load') ->willReturn($routeCollection); - $delegatingLoader = new DelegatingLoader($loaderResolver, ['utf8' => true]); + $delegatingLoader = new DelegatingLoader($loaderResolver, ['utf8' => true], ['_locale' => 'fr|en']); $loadedRouteCollection = $delegatingLoader->load('foo'); $this->assertCount(2, $loadedRouteCollection); @@ -53,6 +47,7 @@ public function testLoadDefaultOptions() 'utf8' => false, ]; $this->assertSame($expected, $routeCollection->get('foo')->getOptions()); + $this->assertSame(['_locale' => 'fr|en'], $routeCollection->get('foo')->getRequirements()); $expected = [ 'compiler_class' => RouteCompiler::class, @@ -60,43 +55,6 @@ public function testLoadDefaultOptions() 'utf8' => true, ]; $this->assertSame($expected, $routeCollection->get('bar')->getOptions()); - } - - /** - * @group legacy - * @expectedDeprecation Referencing controllers with foo:bar:baz is deprecated since Symfony 4.1, use "some_parsed::controller" instead. - */ - public function testLoad() - { - $controllerNameParser = $this->createMock(ControllerNameParser::class); - $controllerNameParser->expects($this->once()) - ->method('parse') - ->with('foo:bar:baz') - ->willReturn('some_parsed::controller'); - - $loaderResolver = $this->createMock(LoaderResolverInterface::class); - - $loader = $this->createMock(LoaderInterface::class); - - $loaderResolver->expects($this->once()) - ->method('resolve') - ->willReturn($loader); - - $routeCollection = new RouteCollection(); - $routeCollection->add('foo', new Route('/', ['_controller' => 'foo:bar:baz'])); - $routeCollection->add('bar', new Route('/', ['_controller' => 'foo::baz'])); - $routeCollection->add('baz', new Route('/', ['_controller' => 'foo:baz'])); - - $loader->expects($this->once()) - ->method('load') - ->willReturn($routeCollection); - - $delegatingLoader = new DelegatingLoader($controllerNameParser, $loaderResolver); - - $loadedRouteCollection = $delegatingLoader->load('foo'); - $this->assertCount(3, $loadedRouteCollection); - $this->assertSame('some_parsed::controller', $routeCollection->get('foo')->getDefault('_controller')); - $this->assertSame('foo::baz', $routeCollection->get('bar')->getDefault('_controller')); - $this->assertSame('foo:baz', $routeCollection->get('baz')->getDefault('_controller')); + $this->assertSame(['_locale' => 'de'], $routeCollection->get('bar')->getRequirements()); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/LegacyRouteLoaderContainerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/LegacyRouteLoaderContainerTest.php deleted file mode 100644 index b0492efd89da4..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/LegacyRouteLoaderContainerTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; - -use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; -use Symfony\Bundle\FrameworkBundle\Routing\LegacyRouteLoaderContainer; -use Symfony\Component\DependencyInjection\Container; - -/** - * @group legacy - */ -class LegacyRouteLoaderContainerTest extends TestCase -{ - /** - * @var ContainerInterface - */ - private $container; - - /** - * @var ContainerInterface - */ - private $serviceLocator; - - /** - * @var LegacyRouteLoaderContainer - */ - private $legacyRouteLoaderContainer; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->container = new Container(); - $this->container->set('foo', new \stdClass()); - - $this->serviceLocator = new Container(); - $this->serviceLocator->set('bar', new \stdClass()); - - $this->legacyRouteLoaderContainer = new LegacyRouteLoaderContainer($this->container, $this->serviceLocator); - } - - /** - * @expectedDeprecation Registering the service route loader "foo" without tagging it with the "routing.route_loader" tag is deprecated since Symfony 4.4 and will be required in Symfony 5.0. - */ - public function testGet() - { - $this->assertSame($this->container->get('foo'), $this->legacyRouteLoaderContainer->get('foo')); - $this->assertSame($this->serviceLocator->get('bar'), $this->legacyRouteLoaderContainer->get('bar')); - } - - public function testHas() - { - $this->assertTrue($this->legacyRouteLoaderContainer->has('foo')); - $this->assertTrue($this->legacyRouteLoaderContainer->has('bar')); - $this->assertFalse($this->legacyRouteLoaderContainer->has('ccc')); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php deleted file mode 100644 index 1c11cf9e09024..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RedirectableUrlMatcherTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher; -use Symfony\Component\Routing\RequestContext; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; - -/** - * @group legacy - */ -class RedirectableUrlMatcherTest extends TestCase -{ - public function testRedirectWhenNoSlash() - { - $coll = new RouteCollection(); - $coll->add('foo', new Route('/foo/')); - - $matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext()); - - $this->assertEquals([ - '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', - 'path' => '/foo/', - 'permanent' => true, - 'scheme' => null, - 'httpPort' => $context->getHttpPort(), - 'httpsPort' => $context->getHttpsPort(), - '_route' => 'foo', - ], - $matcher->match('/foo') - ); - } - - public function testSchemeRedirect() - { - $coll = new RouteCollection(); - $coll->add('foo', new Route('/foo', [], [], [], '', ['https'])); - - $matcher = new RedirectableUrlMatcher($coll, $context = new RequestContext()); - - $this->assertEquals([ - '_controller' => 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction', - 'path' => '/foo', - 'permanent' => true, - 'scheme' => 'https', - 'httpPort' => $context->getHttpPort(), - 'httpsPort' => $context->getHttpsPort(), - '_route' => 'foo', - ], - $matcher->match('/foo') - ); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php index 3e6c5bf6c602e..cdcaa490ac423 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php @@ -396,7 +396,7 @@ public function testExceptionOnNonExistentParameterWithSfContainer() public function testExceptionOnNonStringParameter() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "object".'); + $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "stdClass".'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%object%')); @@ -410,16 +410,24 @@ public function testExceptionOnNonStringParameter() public function testExceptionOnNonStringParameterWithSfContainer() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "object".'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%object%')); $sc = $this->getServiceContainer($routes); - $sc->setParameter('object', new \stdClass()); - $router = new Router($sc, 'foo'); + $pc = $this->createMock(ContainerInterface::class); + $pc + ->expects($this->once()) + ->method('get') + ->willReturn(new \stdClass()) + ; + + $router = new Router($sc, 'foo', [], null, $pc); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "stdClass".'); + $router->getRouteCollection()->get('foo'); } @@ -536,10 +544,6 @@ public function testCacheValidityWithContainerParameters($parameter) $cache = new ResourceCheckerConfigCache($cacheDir.\DIRECTORY_SEPARATOR.'url_matching_routes.php', $resourceCheckers); - if (!$cache->isFresh()) { - $cache = new ResourceCheckerConfigCache($cacheDir.\DIRECTORY_SEPARATOR.'UrlMatcher.php', $resourceCheckers); - } - $this->assertTrue($cache->isFresh()); } finally { if (is_dir($cacheDir)) { @@ -549,6 +553,44 @@ public function testCacheValidityWithContainerParameters($parameter) } } + public function testResolvingSchemes() + { + $routes = new RouteCollection(); + + $route = new Route('/test', [], [], [], '', ['%parameter.http%', '%parameter.https%']); + $routes->add('foo', $route); + + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag([ + 'parameter.http' => 'http', + 'parameter.https' => 'https', + ]); + + $router = new Router($sc, 'foo', [], null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals(['http', 'https'], $route->getSchemes()); + } + + public function testResolvingMethods() + { + $routes = new RouteCollection(); + + $route = new Route('/test', [], [], [], '', [], ['%parameter.get%', '%parameter.post%']); + $routes->add('foo', $route); + + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag([ + 'PARAMETER.GET' => 'GET', + 'PARAMETER.POST' => 'POST', + ]); + + $router = new Router($sc, 'foo', [], null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals(['GET', 'POST'], $route->getMethods()); + } + public function getContainerParameterForRoute() { yield 'String' => ['"foo"']; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php index d494c82e68c4d..62ca08b07ca6b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php @@ -38,7 +38,7 @@ public function testEncryptAndDecrypt() $vault->seal('foo', $plain); unset($_SERVER['foo'], $_ENV['foo']); - (new Dotenv(false))->load($this->envFile); + (new Dotenv())->load($this->envFile); $decrypted = $vault->reveal('foo'); $this->assertSame($plain, $decrypted); @@ -50,7 +50,7 @@ public function testEncryptAndDecrypt() $this->assertFalse($vault->remove('foo')); unset($_SERVER['foo'], $_ENV['foo']); - (new Dotenv(false))->load($this->envFile); + (new Dotenv())->load($this->envFile); $this->assertArrayNotHasKey('foo', $vault->list()); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php index a9b88b1763bd5..de096ccb22de0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php @@ -6,6 +6,9 @@ use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; use Symfony\Component\Filesystem\Filesystem; +/** + * @requires extension sodium + */ class SodiumVaultTest extends TestCase { private $secretsDir; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php deleted file mode 100644 index c26d90770f6bc..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Templating\DelegatingEngine; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Templating\EngineInterface; - -/** - * @group legacy - */ -class DelegatingEngineTest extends TestCase -{ - public function testSupportsRetrievesEngineFromTheContainer() - { - $container = $this->getContainerMock([ - 'engine.first' => $this->getEngineMock('template.php', false), - 'engine.second' => $this->getEngineMock('template.php', true), - ]); - - $delegatingEngine = new DelegatingEngine($container, ['engine.first', 'engine.second']); - - $this->assertTrue($delegatingEngine->supports('template.php')); - } - - public function testGetExistingEngine() - { - $firstEngine = $this->getEngineMock('template.php', false); - $secondEngine = $this->getEngineMock('template.php', true); - $container = $this->getContainerMock([ - 'engine.first' => $firstEngine, - 'engine.second' => $secondEngine, - ]); - - $delegatingEngine = new DelegatingEngine($container, ['engine.first', 'engine.second']); - - $this->assertSame($secondEngine, $delegatingEngine->getEngine('template.php')); - } - - public function testGetInvalidEngine() - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('No engine is able to work with the template "template.php"'); - $firstEngine = $this->getEngineMock('template.php', false); - $secondEngine = $this->getEngineMock('template.php', false); - $container = $this->getContainerMock([ - 'engine.first' => $firstEngine, - 'engine.second' => $secondEngine, - ]); - - $delegatingEngine = new DelegatingEngine($container, ['engine.first', 'engine.second']); - $delegatingEngine->getEngine('template.php'); - } - - public function testRenderResponseWithFrameworkEngine() - { - $response = new Response(); - $engine = $this->getFrameworkEngineMock('template.php', true); - $engine->expects($this->once()) - ->method('renderResponse') - ->with('template.php', ['foo' => 'bar']) - ->willReturn($response); - $container = $this->getContainerMock(['engine' => $engine]); - - $delegatingEngine = new DelegatingEngine($container, ['engine']); - - $this->assertSame($response, $delegatingEngine->renderResponse('template.php', ['foo' => 'bar'])); - } - - public function testRenderResponseWithTemplatingEngine() - { - $engine = $this->getEngineMock('template.php', true); - $container = $this->getContainerMock(['engine' => $engine]); - $delegatingEngine = new DelegatingEngine($container, ['engine']); - - $this->assertInstanceOf(Response::class, $delegatingEngine->renderResponse('template.php', ['foo' => 'bar'])); - } - - private function getEngineMock($template, $supports) - { - $engine = $this->createMock(EngineInterface::class); - - $engine->expects($this->once()) - ->method('supports') - ->with($template) - ->willReturn($supports); - - return $engine; - } - - private function getFrameworkEngineMock($template, $supports) - { - $engine = $this->createMock(\Symfony\Bundle\FrameworkBundle\Templating\EngineInterface::class); - - $engine->expects($this->once()) - ->method('supports') - ->with($template) - ->willReturn($supports); - - return $engine; - } - - private function getContainerMock($services) - { - $container = new ContainerBuilder(); - - foreach ($services as $id => $service) { - $container->set($id, $service); - } - - return $container; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php deleted file mode 100644 index 2e8162baf04f8..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; -use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\TokenInterface; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * @group legacy - */ -class GlobalVariablesTest extends TestCase -{ - private $container; - private $globals; - - protected function setUp(): void - { - $this->container = new Container(); - $this->globals = new GlobalVariables($this->container); - } - - public function testGetTokenNoTokenStorage() - { - $this->assertNull($this->globals->getToken()); - } - - public function testGetTokenNoToken() - { - $tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->container->set('security.token_storage', $tokenStorage); - $this->assertNull($this->globals->getToken()); - } - - public function testGetToken() - { - $tokenStorage = $this->createMock(TokenStorageInterface::class); - - $this->container->set('security.token_storage', $tokenStorage); - - $tokenStorage - ->expects($this->once()) - ->method('getToken') - ->willReturn('token'); - - $this->assertSame('token', $this->globals->getToken()); - } - - public function testGetUserNoTokenStorage() - { - $this->assertNull($this->globals->getUser()); - } - - public function testGetUserNoToken() - { - $tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->container->set('security.token_storage', $tokenStorage); - $this->assertNull($this->globals->getUser()); - } - - /** - * @dataProvider getUserProvider - */ - public function testGetUser($user, $expectedUser) - { - $tokenStorage = $this->createMock(TokenStorageInterface::class); - $token = $this->createMock(TokenInterface::class); - - $this->container->set('security.token_storage', $tokenStorage); - - $token - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - - $tokenStorage - ->expects($this->once()) - ->method('getToken') - ->willReturn($token); - - $this->assertSame($expectedUser, $this->globals->getUser()); - } - - public function getUserProvider() - { - $user = $this->createMock(UserInterface::class); - $std = new \stdClass(); - $token = $this->createMock(TokenInterface::class); - - return [ - [$user, $user], - [$std, $std], - [$token, $token], - ['Anon.', null], - [null, null], - [10, null], - [true, null], - ]; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php deleted file mode 100644 index 8861ff6eb9ff6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\AssetsHelper; -use Symfony\Component\Asset\Package; -use Symfony\Component\Asset\Packages; -use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; - -/** - * @group legacy - */ -class AssetsHelperTest extends TestCase -{ - private $helper; - - protected function setUp(): void - { - $fooPackage = new Package(new StaticVersionStrategy('42', '%s?v=%s')); - $barPackage = new Package(new StaticVersionStrategy('22', '%s?%s')); - - $packages = new Packages($fooPackage, ['bar' => $barPackage]); - - $this->helper = new AssetsHelper($packages); - } - - public function testGetUrl() - { - $this->assertEquals('me.png?v=42', $this->helper->getUrl('me.png')); - $this->assertEquals('me.png?22', $this->helper->getUrl('me.png', 'bar')); - } - - public function testGetVersion() - { - $this->assertEquals('42', $this->helper->getVersion('/')); - $this->assertEquals('22', $this->helper->getVersion('/', 'bar')); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php deleted file mode 100644 index e6b8105806c71..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures; - -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReference; -use Symfony\Component\Templating\TemplateReferenceInterface; - -class StubTemplateNameParser implements TemplateNameParserInterface -{ - private $root; - - private $rootTheme; - - public function __construct($root, $rootTheme) - { - $this->root = $root; - $this->rootTheme = $rootTheme; - } - - public function parse($name): TemplateReferenceInterface - { - list($bundle, $controller, $template) = explode(':', $name, 3); - - if ('_' == $template[0]) { - $path = $this->rootTheme.'/Custom/'.$template; - } elseif ('TestBundle' === $bundle) { - $path = $this->rootTheme.'/'.$controller.'/'.$template; - } else { - $path = $this->root.'/'.$controller.'/'.$template; - } - - return new TemplateReference($path, 'php'); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php deleted file mode 100644 index 2f051f035e548..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures; - -use Symfony\Contracts\Translation\TranslatorInterface; - -class StubTranslator implements TranslatorInterface -{ - public function trans($id, array $parameters = [], $domain = null, $locale = null): string - { - return '[trans]'.strtr($id, $parameters).'[/trans]'; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php deleted file mode 100644 index 33ea9f380f902..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php +++ /dev/null @@ -1,265 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; - -use Symfony\Bundle\FrameworkBundle\FrameworkBundle; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper; -use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser; -use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator; -use Symfony\Component\Form\Extension\Templating\TemplatingExtension; -use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Tests\AbstractDivLayoutTest; -use Symfony\Component\Templating\Loader\FilesystemLoader; -use Symfony\Component\Templating\PhpEngine; - -/** - * @group legacy - */ -class FormHelperDivLayoutTest extends AbstractDivLayoutTest -{ - /** - * @var PhpEngine - */ - protected $engine; - - protected static $supportedFeatureSetVersion = 404; - - protected function getExtensions() - { - // should be moved to the Form component once absolute file paths are supported - // by the default name parser in the Templating component - $reflClass = new \ReflectionClass(FrameworkBundle::class); - $root = realpath(\dirname($reflClass->getFileName()).'/Resources/views'); - $rootTheme = realpath(__DIR__.'/Resources'); - $templateNameParser = new StubTemplateNameParser($root, $rootTheme); - $loader = new FilesystemLoader([]); - - $this->engine = new PhpEngine($templateNameParser, $loader); - $this->engine->addGlobal('global', ''); - $this->engine->setHelpers([ - new TranslatorHelper(new StubTranslator()), - ]); - - return array_merge(parent::getExtensions(), [ - new TemplatingExtension($this->engine, $this->csrfTokenManager, [ - 'FrameworkBundle:Form', - ]), - ]); - } - - protected function tearDown(): void - { - $this->engine = null; - - parent::tearDown(); - } - - public function testStartTagHasNoActionAttributeWhenActionIsEmpty() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => '', - ]); - - $html = $this->renderStart($form->createView()); - - $this->assertSame('', $html); - } - - public function testStartTagHasActionAttributeWhenActionIsZero() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => '0', - ]); - - $html = $this->renderStart($form->createView()); - - $this->assertSame('', $html); - } - - public function testMoneyWidgetInIso() - { - $this->engine->setCharset('ISO-8859-1'); - - $view = $this->factory - ->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\MoneyType') - ->createView() - ; - - $this->assertSame('€ ', $this->renderWidget($view)); - } - - public function testHelpAttr() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - 'help_attr' => [ - 'class' => 'class-test', - ], - ]); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="class-test help-text"] - [.="[trans]Help text test![/trans]"] -' - ); - } - - public function testHelpHtmlDefaultIsFalse() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - ]); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - [.="[trans]Help text test![/trans]"] -' - ); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - /b - [.="text"] -', 0 - ); - } - - public function testHelpHtmlIsFalse() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - 'help_html' => false, - ]); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - [.="[trans]Help text test![/trans]"] -' - ); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - /b - [.="text"] -', 0 - ); - } - - public function testHelpHtmlIsTrue() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - 'help_html' => true, - ]); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - [.="[trans]Help text test![/trans]"] -', 0 - ); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="help-text"] - /b - [.="text"] -' - ); - } - - protected function renderForm(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->form($view, $vars); - } - - protected function renderLabel(FormView $view, $label = null, array $vars = []) - { - return (string) $this->engine->get('form')->label($view, $label, $vars); - } - - protected function renderHelp(FormView $view) - { - return (string) $this->engine->get('form')->help($view); - } - - protected function renderErrors(FormView $view) - { - return (string) $this->engine->get('form')->errors($view); - } - - protected function renderWidget(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->widget($view, $vars); - } - - protected function renderRow(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->row($view, $vars); - } - - protected function renderRest(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->rest($view, $vars); - } - - protected function renderStart(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->start($view, $vars); - } - - protected function renderEnd(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->end($view, $vars); - } - - protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) - { - $this->engine->get('form')->setTheme($view, $themes, $useDefaultThemes); - } - - public static function themeBlockInheritanceProvider() - { - return [ - [['TestBundle:Parent']], - ]; - } - - public static function themeInheritanceProvider() - { - return [ - [['TestBundle:Parent'], ['TestBundle:Child']], - ]; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php deleted file mode 100644 index f985efce55dca..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php +++ /dev/null @@ -1,160 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; - -use Symfony\Bundle\FrameworkBundle\FrameworkBundle; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper; -use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser; -use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator; -use Symfony\Component\Form\Extension\Templating\TemplatingExtension; -use Symfony\Component\Form\FormView; -use Symfony\Component\Form\Tests\AbstractTableLayoutTest; -use Symfony\Component\Templating\Loader\FilesystemLoader; -use Symfony\Component\Templating\PhpEngine; - -/** - * @group legacy - */ -class FormHelperTableLayoutTest extends AbstractTableLayoutTest -{ - /** - * @var PhpEngine - */ - protected $engine; - - protected static $supportedFeatureSetVersion = 404; - - public function testStartTagHasNoActionAttributeWhenActionIsEmpty() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => '', - ]); - - $html = $this->renderStart($form->createView()); - - $this->assertSame('', $html); - } - - public function testStartTagHasActionAttributeWhenActionIsZero() - { - $form = $this->factory->create('Symfony\Component\Form\Extension\Core\Type\FormType', null, [ - 'method' => 'get', - 'action' => '0', - ]); - - $html = $this->renderStart($form->createView()); - - $this->assertSame('', $html); - } - - public function testHelpAttr() - { - $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', null, [ - 'help' => 'Help text test!', - 'help_attr' => [ - 'class' => 'class-test', - ], - ]); - $view = $form->createView(); - $html = $this->renderHelp($view); - - $this->assertMatchesXpath($html, - '/p - [@id="name_help"] - [@class="class-test help-text"] - [.="[trans]Help text test![/trans]"] -' - ); - } - - protected function getExtensions() - { - // should be moved to the Form component once absolute file paths are supported - // by the default name parser in the Templating component - $reflClass = new \ReflectionClass(FrameworkBundle::class); - $root = realpath(\dirname($reflClass->getFileName()).'/Resources/views'); - $rootTheme = realpath(__DIR__.'/Resources'); - $templateNameParser = new StubTemplateNameParser($root, $rootTheme); - $loader = new FilesystemLoader([]); - - $this->engine = new PhpEngine($templateNameParser, $loader); - $this->engine->addGlobal('global', ''); - $this->engine->setHelpers([ - new TranslatorHelper(new StubTranslator()), - ]); - - return array_merge(parent::getExtensions(), [ - new TemplatingExtension($this->engine, $this->csrfTokenManager, [ - 'FrameworkBundle:Form', - 'FrameworkBundle:FormTable', - ]), - ]); - } - - protected function tearDown(): void - { - $this->engine = null; - - parent::tearDown(); - } - - protected function renderForm(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->form($view, $vars); - } - - protected function renderLabel(FormView $view, $label = null, array $vars = []) - { - return (string) $this->engine->get('form')->label($view, $label, $vars); - } - - protected function renderHelp(FormView $view) - { - return (string) $this->engine->get('form')->help($view); - } - - protected function renderErrors(FormView $view) - { - return (string) $this->engine->get('form')->errors($view); - } - - protected function renderWidget(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->widget($view, $vars); - } - - protected function renderRow(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->row($view, $vars); - } - - protected function renderRest(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->rest($view, $vars); - } - - protected function renderStart(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->start($view, $vars); - } - - protected function renderEnd(FormView $view, array $vars = []) - { - return (string) $this->engine->get('form')->end($view, $vars); - } - - protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) - { - $this->engine->get('form')->setTheme($view, $themes, $useDefaultThemes); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php deleted file mode 100644 index cddb14e5f9df9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\RequestHelper; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; - -/** - * @group legacy - */ -class RequestHelperTest extends TestCase -{ - protected $requestStack; - - protected function setUp(): void - { - $this->requestStack = new RequestStack(); - $request = new Request(); - $request->initialize(['foobar' => 'bar']); - $this->requestStack->push($request); - } - - public function testGetParameter() - { - $helper = new RequestHelper($this->requestStack); - - $this->assertEquals('bar', $helper->getParameter('foobar')); - $this->assertEquals('foo', $helper->getParameter('bar', 'foo')); - - $this->assertNull($helper->getParameter('foo')); - } - - public function testGetLocale() - { - $helper = new RequestHelper($this->requestStack); - - $this->assertEquals('en', $helper->getLocale()); - } - - public function testGetName() - { - $helper = new RequestHelper($this->requestStack); - - $this->assertEquals('request', $helper->getName()); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Child/form_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Child/form_label.html.php deleted file mode 100644 index aebb53d3e7221..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Child/form_label.html.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_c_entry_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_c_entry_label.html.php deleted file mode 100644 index 4ad7e75ddcc12..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_c_entry_label.html.php +++ /dev/null @@ -1,2 +0,0 @@ -humanize($name); } ?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_names_entry_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_names_entry_label.html.php deleted file mode 100644 index 71de9d4631de7..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_names_entry_label.html.php +++ /dev/null @@ -1,4 +0,0 @@ -humanize($name); -} ?> - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_text_id_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_text_id_widget.html.php deleted file mode 100644 index 078fe57583f1c..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_text_id_widget.html.php +++ /dev/null @@ -1,3 +0,0 @@ -
- widget($form) ?> -
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_label.html.php deleted file mode 100644 index 068c5dec3ff48..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_label.html.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php deleted file mode 100644 index 235028ee00fc2..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php +++ /dev/null @@ -1,2 +0,0 @@ - -block($form, 'widget_attributes'); ?> value="" rel="theme" /> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php deleted file mode 100644 index 0ee9930efddf2..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\SessionHelper; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; - -/** - * @group legacy - */ -class SessionHelperTest extends TestCase -{ - protected $requestStack; - - protected function setUp(): void - { - $request = new Request(); - - $session = new Session(new MockArraySessionStorage()); - $session->set('foobar', 'bar'); - $session->getFlashBag()->set('notice', 'bar'); - - $request->setSession($session); - - $this->requestStack = new RequestStack(); - $this->requestStack->push($request); - } - - protected function tearDown(): void - { - $this->requestStack = null; - } - - public function testFlash() - { - $helper = new SessionHelper($this->requestStack); - - $this->assertTrue($helper->hasFlash('notice')); - - $this->assertEquals(['bar'], $helper->getFlash('notice')); - } - - public function testGetFlashes() - { - $helper = new SessionHelper($this->requestStack); - $this->assertEquals(['notice' => ['bar']], $helper->getFlashes()); - } - - public function testGet() - { - $helper = new SessionHelper($this->requestStack); - - $this->assertEquals('bar', $helper->get('foobar')); - $this->assertEquals('foo', $helper->get('bar', 'foo')); - - $this->assertNull($helper->get('foo')); - } - - public function testGetName() - { - $helper = new SessionHelper($this->requestStack); - - $this->assertEquals('session', $helper->getName()); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php deleted file mode 100644 index 074916ed75156..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Templating\Helper\StopwatchHelper; -use Symfony\Component\Stopwatch\Stopwatch; - -/** - * @group legacy - */ -class StopwatchHelperTest extends TestCase -{ - public function testDevEnvironment() - { - $stopwatch = $this->createMock(Stopwatch::class); - $stopwatch->expects($this->once()) - ->method('start') - ->with('foo'); - - $helper = new StopwatchHelper($stopwatch); - $helper->start('foo'); - } - - public function testProdEnvironment() - { - $helper = new StopwatchHelper(null); - $helper->start('foo'); - - // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above - // can be executed without throwing any exceptions - $this->addToAssertionCount(1); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php deleted file mode 100644 index 5e0cea2cd12c9..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php +++ /dev/null @@ -1,100 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Loader; - -use Symfony\Bundle\FrameworkBundle\Templating\Loader\TemplateLocator; -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\Config\FileLocator; - -/** - * @group legacy - */ -class TemplateLocatorTest extends TestCase -{ - public function testLocateATemplate() - { - $template = new TemplateReference('bundle', 'controller', 'name', 'format', 'engine'); - - $fileLocator = $this->getFileLocator(); - - $fileLocator - ->expects($this->once()) - ->method('locate') - ->with($template->getPath()) - ->willReturn('/path/to/template') - ; - - $locator = new TemplateLocator($fileLocator); - - $this->assertEquals('/path/to/template', $locator->locate($template)); - - // Assert cache is used as $fileLocator->locate should be called only once - $this->assertEquals('/path/to/template', $locator->locate($template)); - } - - public function testLocateATemplateFromCacheDir() - { - $template = new TemplateReference('bundle', 'controller', 'name', 'format', 'engine'); - - $fileLocator = $this->getFileLocator(); - - $locator = new TemplateLocator($fileLocator, __DIR__.'/../../Fixtures'); - - $this->assertEquals(realpath(__DIR__.'/../../Fixtures/Resources/views/this.is.a.template.format.engine'), $locator->locate($template)); - } - - public function testThrowsExceptionWhenTemplateNotFound() - { - $template = new TemplateReference('bundle', 'controller', 'name', 'format', 'engine'); - - $fileLocator = $this->getFileLocator(); - - $errorMessage = 'FileLocator exception message'; - - $fileLocator - ->expects($this->once()) - ->method('locate') - ->willThrowException(new \InvalidArgumentException($errorMessage)) - ; - - $locator = new TemplateLocator($fileLocator); - - try { - $locator->locate($template); - $this->fail('->locate() should throw an exception when the file is not found.'); - } catch (\InvalidArgumentException $e) { - $this->assertStringContainsString( - $errorMessage, - $e->getMessage(), - 'TemplateLocator exception should propagate the FileLocator exception message' - ); - } - } - - public function testThrowsAnExceptionWhenTemplateIsNotATemplateReferenceInterface() - { - $this->expectException(\InvalidArgumentException::class); - $locator = new TemplateLocator($this->getFileLocator()); - $locator->locate('template'); - } - - protected function getFileLocator() - { - return $this - ->getMockBuilder(FileLocator::class) - ->setMethods(['locate']) - ->setConstructorArgs(['/path/to/fallback']) - ->getMock() - ; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php deleted file mode 100644 index 8c39fb08279d6..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; -use Symfony\Bundle\FrameworkBundle\Templating\PhpEngine; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; -use Symfony\Component\Templating\Loader\Loader; -use Symfony\Component\Templating\TemplateNameParser; - -/** - * @group legacy - */ -class PhpEngineTest extends TestCase -{ - public function testEvaluateAddsAppGlobal() - { - $container = $this->getContainer(); - $loader = $this->getMockForAbstractClass(Loader::class); - $engine = new PhpEngine(new TemplateNameParser(), $container, $loader, $app = new GlobalVariables($container)); - $globals = $engine->getGlobals(); - $this->assertSame($app, $globals['app']); - } - - public function testEvaluateWithoutAvailableRequest() - { - $container = new Container(); - $loader = $this->getMockForAbstractClass(Loader::class); - $engine = new PhpEngine(new TemplateNameParser(), $container, $loader, new GlobalVariables($container)); - - $this->assertFalse($container->has('request_stack')); - $globals = $engine->getGlobals(); - $this->assertEmpty($globals['app']->getRequest()); - } - - public function testGetInvalidHelper() - { - $this->expectException(\InvalidArgumentException::class); - $container = $this->getContainer(); - $loader = $this->getMockForAbstractClass(Loader::class); - $engine = new PhpEngine(new TemplateNameParser(), $container, $loader); - - $engine->get('non-existing-helper'); - } - - /** - * Creates a Container with a Session-containing Request service. - */ - protected function getContainer(): Container - { - $container = new Container(); - $session = new Session(new MockArraySessionStorage()); - $request = new Request(); - $stack = new RequestStack(); - $stack->push($request); - - $request->setSession($session); - $container->set('request_stack', $stack); - - return $container; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php deleted file mode 100644 index 58e671ddf358b..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\TemplateFilenameParser; -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; - -/** - * @group legacy - */ -class TemplateFilenameParserTest extends TestCase -{ - protected $parser; - - protected function setUp(): void - { - $this->parser = new TemplateFilenameParser(); - } - - protected function tearDown(): void - { - $this->parser = null; - } - - /** - * @dataProvider getFilenameToTemplateProvider - */ - public function testParseFromFilename($file, $ref) - { - $template = $this->parser->parse($file); - - if (false === $ref) { - $this->assertFalse($template); - } else { - $this->assertEquals($template->getLogicalName(), $ref->getLogicalName()); - } - } - - public function getFilenameToTemplateProvider() - { - return [ - ['/path/to/section/name.format.engine', new TemplateReference('', '/path/to/section', 'name', 'format', 'engine')], - ['\\path\\to\\section\\name.format.engine', new TemplateReference('', '/path/to/section', 'name', 'format', 'engine')], - ['name.format.engine', new TemplateReference('', '', 'name', 'format', 'engine')], - ['name.format', false], - ['name', false], - ]; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php deleted file mode 100644 index 4fe8aa3a4367e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php +++ /dev/null @@ -1,86 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\TemplateNameParser; -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Templating\TemplateReference as BaseTemplateReference; - -/** - * @group legacy - */ -class TemplateNameParserTest extends TestCase -{ - protected $parser; - - protected function setUp(): void - { - $kernel = $this->createMock(KernelInterface::class); - $kernel - ->expects($this->any()) - ->method('getBundle') - ->willReturnCallback(function ($bundle) { - if (\in_array($bundle, ['SensioFooBundle', 'SensioCmsFooBundle', 'FooBundle'])) { - return true; - } - - throw new \InvalidArgumentException(); - }) - ; - $this->parser = new TemplateNameParser($kernel); - } - - protected function tearDown(): void - { - $this->parser = null; - } - - /** - * @dataProvider parseProvider - */ - public function testParse($name, $logicalName, $path, $ref) - { - $template = $this->parser->parse($name); - - $this->assertSame($ref->getLogicalName(), $template->getLogicalName()); - $this->assertSame($logicalName, $template->getLogicalName()); - $this->assertSame($path, $template->getPath()); - } - - public function parseProvider() - { - return [ - ['FooBundle:Post:index.html.php', 'FooBundle:Post:index.html.php', '@FooBundle/Resources/views/Post/index.html.php', new TemplateReference('FooBundle', 'Post', 'index', 'html', 'php')], - ['FooBundle:Post:index.html.twig', 'FooBundle:Post:index.html.twig', '@FooBundle/Resources/views/Post/index.html.twig', new TemplateReference('FooBundle', 'Post', 'index', 'html', 'twig')], - ['FooBundle:Post:index.xml.php', 'FooBundle:Post:index.xml.php', '@FooBundle/Resources/views/Post/index.xml.php', new TemplateReference('FooBundle', 'Post', 'index', 'xml', 'php')], - ['SensioFooBundle:Post:index.html.php', 'SensioFooBundle:Post:index.html.php', '@SensioFooBundle/Resources/views/Post/index.html.php', new TemplateReference('SensioFooBundle', 'Post', 'index', 'html', 'php')], - ['SensioCmsFooBundle:Post:index.html.php', 'SensioCmsFooBundle:Post:index.html.php', '@SensioCmsFooBundle/Resources/views/Post/index.html.php', new TemplateReference('SensioCmsFooBundle', 'Post', 'index', 'html', 'php')], - [':Post:index.html.php', ':Post:index.html.php', 'views/Post/index.html.php', new TemplateReference('', 'Post', 'index', 'html', 'php')], - ['::index.html.php', '::index.html.php', 'views/index.html.php', new TemplateReference('', '', 'index', 'html', 'php')], - ['index.html.php', '::index.html.php', 'views/index.html.php', new TemplateReference('', '', 'index', 'html', 'php')], - ['FooBundle:Post:foo.bar.index.html.php', 'FooBundle:Post:foo.bar.index.html.php', '@FooBundle/Resources/views/Post/foo.bar.index.html.php', new TemplateReference('FooBundle', 'Post', 'foo.bar.index', 'html', 'php')], - ['@FooBundle/Resources/views/layout.html.twig', '@FooBundle/Resources/views/layout.html.twig', '@FooBundle/Resources/views/layout.html.twig', new BaseTemplateReference('@FooBundle/Resources/views/layout.html.twig', 'twig')], - ['@FooBundle/Foo/layout.html.twig', '@FooBundle/Foo/layout.html.twig', '@FooBundle/Foo/layout.html.twig', new BaseTemplateReference('@FooBundle/Foo/layout.html.twig', 'twig')], - ['name.twig', 'name.twig', 'name.twig', new BaseTemplateReference('name.twig', 'twig')], - ['name', 'name', 'name', new BaseTemplateReference('name')], - ['default/index.html.php', '::default/index.html.php', 'views/default/index.html.php', new TemplateReference(null, null, 'default/index', 'html', 'php')], - ]; - } - - public function testParseValidNameWithNotFoundBundle() - { - $this->expectException(\InvalidArgumentException::class); - $this->parser->parse('BarBundle:Post:index.html.php'); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateReferenceTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateReferenceTest.php deleted file mode 100644 index 179c3b6da0dbd..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateReferenceTest.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; - -/** - * @group legacy - */ -class TemplateReferenceTest extends TestCase -{ - public function testGetPathWorksWithNamespacedControllers() - { - $reference = new TemplateReference('AcmeBlogBundle', 'Admin\Post', 'index', 'html', 'twig'); - - $this->assertSame( - '@AcmeBlogBundle/Resources/views/Admin/Post/index.html.twig', - $reference->getPath() - ); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateTest.php deleted file mode 100644 index 899740169f207..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateTest.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; - -/** - * @group legacy - */ -class TemplateTest extends TestCase -{ - /** - * @dataProvider getTemplateToPathProvider - */ - public function testGetPathForTemplate($template, $path) - { - $this->assertSame($template->getPath(), $path); - } - - public function getTemplateToPathProvider() - { - return [ - [new TemplateReference('FooBundle', 'Post', 'index', 'html', 'php'), '@FooBundle/Resources/views/Post/index.html.php'], - [new TemplateReference('FooBundle', '', 'index', 'html', 'twig'), '@FooBundle/Resources/views/index.html.twig'], - [new TemplateReference('', 'Post', 'index', 'html', 'php'), 'views/Post/index.html.php'], - [new TemplateReference('', '', 'index', 'html', 'php'), 'views/index.html.php'], - ]; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php deleted file mode 100644 index 2db6d689530ff..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; - -use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; -use Symfony\Bundle\FrameworkBundle\Templating\TimedPhpEngine; -use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\Stopwatch\Stopwatch; -use Symfony\Component\Templating\Loader\Loader; -use Symfony\Component\Templating\Storage\StringStorage; -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReferenceInterface; - -/** - * @group legacy - */ -class TimedPhpEngineTest extends TestCase -{ - public function testThatRenderLogsTime() - { - $container = $this->createMock(Container::class); - $templateNameParser = $this->getTemplateNameParser(); - $globalVariables = $this->getGlobalVariables(); - $loader = $this->getLoader(new StringStorage('foo')); - - $stopwatch = new Stopwatch(); - - $engine = new TimedPhpEngine($templateNameParser, $container, $loader, $stopwatch, $globalVariables); - $engine->render('index.php'); - - $sections = $stopwatch->getSections(); - - $this->assertCount(1, $sections); - $this->assertCount(1, reset($sections)->getEvents()); - } - - private function getTemplateNameParser(): TemplateNameParserInterface - { - $templateReference = $this->createMock(TemplateReferenceInterface::class); - $templateNameParser = $this->createMock(TemplateNameParserInterface::class); - $templateNameParser->expects($this->any()) - ->method('parse') - ->willReturn($templateReference); - - return $templateNameParser; - } - - private function getGlobalVariables(): GlobalVariables - { - return $this->createMock(GlobalVariables::class); - } - - private function getLoader(StringStorage $storage): Loader - { - $loader = $this->getMockForAbstractClass(Loader::class); - $loader->expects($this->once()) - ->method('load') - ->willReturn($storage); - - return $loader; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/TestBrowserTokenTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/TestBrowserTokenTest.php new file mode 100644 index 0000000000000..8c5387590a43a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/TestBrowserTokenTest.php @@ -0,0 +1,16 @@ +assertSame('main', $token->getFirewallName()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php index ff88b34007069..f6098ea6e8eca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Test; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait; @@ -22,6 +23,7 @@ use Symfony\Component\HttpFoundation\Cookie as HttpFoundationCookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Test\Constraint\ResponseFormatSame; class WebTestCaseTest extends TestCase { @@ -74,6 +76,20 @@ public function testAssertResponseRedirectsWithLocationAndStatusCode() $this->getResponseTester(new Response('', 302))->assertResponseRedirects('https://example.com/', 301); } + public function testAssertResponseFormat() + { + if (!class_exists(ResponseFormatSame::class)) { + $this->markTestSkipped('Too old version of HttpFoundation.'); + } + + $this->getResponseTester(new Response('', 200, ['Content-Type' => 'application/vnd.myformat']))->assertResponseFormatSame('custom'); + $this->getResponseTester(new Response('', 200, ['Content-Type' => 'application/ld+json']))->assertResponseFormatSame('jsonld'); + $this->getResponseTester(new Response())->assertResponseFormatSame(null); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Failed asserting that the Response format is jsonld.\nHTTP/1.0 200 OK"); + $this->getResponseTester(new Response())->assertResponseFormatSame('jsonld'); + } + public function testAssertResponseHasHeader() { $this->getResponseTester(new Response())->assertResponseHasHeader('Date'); @@ -219,6 +235,38 @@ public function testAssertInputValueNotSame() $this->getCrawlerTester(new Crawler(''))->assertInputValueNotSame('password', 'pa$$'); } + public function testAssertCheckboxChecked() + { + $this->getCrawlerTester(new Crawler(''))->assertCheckboxChecked('rememberMe'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('matches selector "input[name="rememberMe"]" and has a node matching selector "input[name="rememberMe"]" with attribute "checked" of value "checked".'); + $this->getCrawlerTester(new Crawler(''))->assertCheckboxChecked('rememberMe'); + } + + public function testAssertCheckboxNotChecked() + { + $this->getCrawlerTester(new Crawler(''))->assertCheckboxNotChecked('rememberMe'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('matches selector "input[name="rememberMe"]" and does not have a node matching selector "input[name="rememberMe"]" with attribute "checked" of value "checked".'); + $this->getCrawlerTester(new Crawler(''))->assertCheckboxNotChecked('rememberMe'); + } + + public function testAssertFormValue() + { + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertFormValue('#form', 'username', 'Fabien'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that two strings are identical.'); + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertFormValue('#form', 'username', 'Jane'); + } + + public function testAssertNoFormValue() + { + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe'); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Field "rememberMe" has a value in form "#form".'); + $this->getCrawlerTester(new Crawler('', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe'); + } + public function testAssertRequestAttributeValueSame() { $this->getRequestTester()->assertRequestAttributeValueSame('foo', 'bar'); @@ -235,11 +283,26 @@ public function testAssertRouteSame() $this->getRequestTester()->assertRouteSame('articles'); } + public function testExceptionOnServerError() + { + try { + $this->getResponseTester(new Response('', 500, ['X-Debug-Exception' => 'An exception has occurred', 'X-Debug-Exception-File' => '%2Fsrv%2Ftest.php:12']))->assertResponseIsSuccessful(); + } catch (ExpectationFailedException $exception) { + $this->assertSame('An exception has occurred', $exception->getPrevious()->getMessage()); + $this->assertSame('/srv/test.php', $exception->getPrevious()->getFile()); + $this->assertSame(12, $exception->getPrevious()->getLine()); + } + } + private function getResponseTester(Response $response): WebTestCase { $client = $this->createMock(KernelBrowser::class); $client->expects($this->any())->method('getResponse')->willReturn($response); + $request = new Request(); + $request->setFormat('custom', ['application/vnd.myformat']); + $client->expects($this->any())->method('getRequest')->willReturn($request); + return $this->getTester($client); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index 9c3f2e78d610f..54416a343b88e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Loader\YamlFileLoader; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Contracts\Translation\TranslatorInterface; class TranslatorTest extends TestCase { @@ -65,19 +66,6 @@ public function testTransWithoutCaching() $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); } - /** - * @group legacy - */ - public function testTransChoiceWithoutCaching() - { - $translator = $this->getTranslator($this->getLoader()); - $translator->setLocale('fr'); - $translator->setFallbackLocales(['en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin']); - - $this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0)); - $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); - } - public function testTransWithCaching() { // prime the cache @@ -112,31 +100,6 @@ public function testTransWithCaching() $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); } - /** - * @group legacy - */ - public function testTransChoiceWithCaching() - { - // prime the cache - $translator = $this->getTranslator($this->getLoader(), ['cache_dir' => $this->tmpDir]); - $translator->setLocale('fr'); - $translator->setFallbackLocales(['en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin']); - - $this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0)); - $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); - - // do it another time as the cache is primed now - $loader = $this->createMock(LoaderInterface::class); - $loader->expects($this->never())->method('load'); - - $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir]); - $translator->setLocale('fr'); - $translator->setFallbackLocales(['en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin']); - - $this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0)); - $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); - } - public function testTransWithCachingWithInvalidLocale() { $this->expectException(\InvalidArgumentException::class); @@ -348,9 +311,9 @@ protected function getContainer($loader) return $container; } - public function getTranslator($loader, $options = [], $loaderFomat = 'loader', $translatorClass = Translator::class, $defaultLocale = 'en') + public function getTranslator($loader, $options = [], $loaderFomat = 'loader', $translatorClass = Translator::class, $defaultLocale = 'en', array $enabledLocales = []): TranslatorInterface { - $translator = $this->createTranslator($loader, $options, $translatorClass, $loaderFomat, $defaultLocale); + $translator = $this->createTranslator($loader, $options, $translatorClass, $loaderFomat, $defaultLocale, $enabledLocales); if ('loader' === $loaderFomat) { $translator->addResource('loader', 'foo', 'fr'); @@ -390,6 +353,31 @@ public function testWarmup() $this->assertEquals('répertoire', $translator->trans('folder')); } + public function testEnabledLocales() + { + $loader = new YamlFileLoader(); + $resourceFiles = [ + 'fr' => [ + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ], + ]; + + // prime the cache without configuring the enabled locales + $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles], 'yml', Translator::class, 'en', []); + $translator->setFallbackLocales(['fr']); + $translator->warmup($this->tmpDir); + + $this->assertCount(2, glob($this->tmpDir.'/catalogue.*.*.php'), 'Both "en" and "fr" catalogues are generated.'); + + // prime the cache and configure the enabled locales + $this->deleteTmpDir(); + $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles], 'yml', Translator::class, 'en', ['fr']); + $translator->setFallbackLocales(['fr']); + $translator->warmup($this->tmpDir); + + $this->assertCount(1, glob($this->tmpDir.'/catalogue.*.*.php'), 'Only the "fr" catalogue is generated.'); + } + public function testLoadingTranslationFilesWithDotsInMessageDomain() { $loader = new YamlFileLoader(); @@ -405,14 +393,15 @@ public function testLoadingTranslationFilesWithDotsInMessageDomain() $this->assertEquals('It works!', $translator->trans('message', [], 'domain.with.dots')); } - private function createTranslator($loader, $options, $translatorClass = Translator::class, $loaderFomat = 'loader', $defaultLocale = 'en') + private function createTranslator($loader, $options, $translatorClass = Translator::class, $loaderFomat = 'loader', $defaultLocale = 'en', array $enabledLocales = []) { if (null === $defaultLocale) { return new $translatorClass( $this->getContainer($loader), new MessageFormatter(), [$loaderFomat => [$loaderFomat]], - $options + $options, + $enabledLocales ); } @@ -421,7 +410,8 @@ private function createTranslator($loader, $options, $translatorClass = Translat new MessageFormatter(), $defaultLocale, [$loaderFomat => [$loaderFomat]], - $options + $options, + $enabledLocales ); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index cd5444034ba7c..b4d9a664a5e02 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -36,24 +36,32 @@ class Translator extends BaseTranslator implements WarmableInterface ]; /** - * @var array + * @var list */ - private $resourceLocales; + private array $resourceLocales; /** * Holds parameters from addResource() calls so we can defer the actual * parent::addResource() calls until initialize() is executed. * - * @var array + * @var array[] */ - private $resources = []; + private array $resources = []; - private $resourceFiles; + /** + * @var string[][] + */ + private array $resourceFiles; /** * @var string[] */ - private $scannedDirectories; + private array $scannedDirectories; + + /** + * @var string[] + */ + private array $enabledLocales; /** * Constructor. @@ -67,10 +75,11 @@ class Translator extends BaseTranslator implements WarmableInterface * * @throws InvalidArgumentException */ - public function __construct(ContainerInterface $container, MessageFormatterInterface $formatter, string $defaultLocale, array $loaderIds = [], array $options = []) + public function __construct(ContainerInterface $container, MessageFormatterInterface $formatter, string $defaultLocale, array $loaderIds = [], array $options = [], array $enabledLocales = []) { $this->container = $container; $this->loaderIds = $loaderIds; + $this->enabledLocales = $enabledLocales; // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { @@ -87,16 +96,19 @@ public function __construct(ContainerInterface $container, MessageFormatterInter /** * {@inheritdoc} + * + * @return string[] */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir): array { // skip warmUp when translator doesn't use cache if (null === $this->options['cache_dir']) { - return; + return []; } - $locales = array_merge($this->getFallbackLocales(), [$this->getLocale()], $this->resourceLocales); - foreach (array_unique($locales) as $locale) { + $localesToWarmUp = $this->enabledLocales ?: array_merge($this->getFallbackLocales(), [$this->getLocale()], $this->resourceLocales); + + foreach (array_unique($localesToWarmUp) as $locale) { // reset catalogue in case it's already loaded during the dump of the other locales. if (isset($this->catalogues[$locale])) { unset($this->catalogues[$locale]); @@ -104,9 +116,11 @@ public function warmUp($cacheDir) $this->loadCatalogue($locale); } + + return []; } - public function addResource($format, $resource, $locale, $domain = null) + public function addResource(string $format, mixed $resource, string $locale, string $domain = null) { if ($this->resourceFiles) { $this->addResourceFiles(); @@ -117,7 +131,7 @@ public function addResource($format, $resource, $locale, $domain = null) /** * {@inheritdoc} */ - protected function initializeCatalogue($locale) + protected function initializeCatalogue(string $locale) { $this->initialize(); parent::initializeCatalogue($locale); @@ -141,7 +155,7 @@ protected function initialize() if ($this->resourceFiles) { $this->addResourceFiles(); } - foreach ($this->resources as $key => $params) { + foreach ($this->resources as $params) { [$format, $resource, $locale, $domain] = $params; parent::addResource($format, $resource, $locale, $domain); } @@ -154,13 +168,13 @@ protected function initialize() } } - private function addResourceFiles() + private function addResourceFiles(): void { $filesByLocale = $this->resourceFiles; $this->resourceFiles = []; - foreach ($filesByLocale as $locale => $files) { - foreach ($files as $key => $file) { + foreach ($filesByLocale as $files) { + foreach ($files as $file) { // filename is domain.locale.format $fileNameParts = explode('.', basename($file)); $format = array_pop($fileNameParts); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 1e382f1dcf0cb..5c925160733b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -16,82 +16,85 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=8.0.2", + "composer-runtime-api": ">=2.1", "ext-xml": "*", - "symfony/cache": "^4.4|^5.0", - "symfony/config": "^4.4.11|~5.0.11|^5.1.3", - "symfony/dependency-injection": "^4.4.38|^5.0.1", - "symfony/error-handler": "^4.4.1|^5.0.1", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/http-kernel": "^4.4", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4.5|^6.0.5", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/filesystem": "^3.4|^4.0|^5.0", - "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/routing": "^4.4.12|^5.1.4" + "symfony/polyfill-php81": "^1.22", + "symfony/filesystem": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0" }, "require-dev": { - "doctrine/annotations": "^1.10.4", - "doctrine/cache": "^1.0|^2.0", + "doctrine/annotations": "^1.13.1", "doctrine/persistence": "^1.3|^2.0", - "paragonie/sodium_compat": "^1.8", - "symfony/asset": "^3.4|^4.0|^5.0", - "symfony/browser-kit": "^4.3|^5.0", - "symfony/console": "^4.4.21|^5.0", - "symfony/css-selector": "^3.4|^4.0|^5.0", - "symfony/dom-crawler": "^4.4.30|^5.3.7", - "symfony/dotenv": "^4.3.6|^5.0", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/dotenv": "^5.4|^6.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/form": "^4.3.5|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/mailer": "^4.4|^5.0", - "symfony/messenger": "^4.4|^5.0", - "symfony/mime": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/security-core": "^3.4|^4.4|^5.2", - "symfony/security-csrf": "^3.4|^4.0|^5.0", - "symfony/security-http": "^3.4|^4.0|^5.0", - "symfony/serializer": "^4.4|^5.0", - "symfony/stopwatch": "^3.4|^4.0|^5.0", - "symfony/translation": "^4.4|^5.0", - "symfony/templating": "^3.4|^4.0|^5.0", - "symfony/twig-bundle": "^4.4|^5.0", - "symfony/validator": "^4.4|^5.0", - "symfony/workflow": "^4.3.6|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0", - "symfony/property-info": "^3.4|^4.0|^5.0", - "symfony/web-link": "^4.4|^5.0", + "symfony/form": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/notifier": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/security-bundle": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/stopwatch": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/workflow": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/web-link": "^5.4|^6.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "twig/twig": "^1.43|^2.13|^3.0.4" + "twig/twig": "^2.10|^3.0", + "symfony/phpunit-bridge": "^5.4|^6.0" }, "conflict": { + "doctrine/annotations": "<1.13.1", "doctrine/persistence": "<1.3", - "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", - "phpdocumentor/type-resolver": "<0.3.0|1.3.*", - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", - "symfony/asset": "<3.4", - "symfony/browser-kit": "<4.3", - "symfony/console": "<4.4.21", - "symfony/dotenv": "<4.3.6", - "symfony/dom-crawler": "<4.3", - "symfony/http-client": "<4.4", - "symfony/form": "<4.3.5", - "symfony/lock": "<4.4", - "symfony/mailer": "<4.4", - "symfony/messenger": "<4.4", - "symfony/mime": "<4.4", - "symfony/property-info": "<3.4", - "symfony/security-bundle": "<4.4", - "symfony/serializer": "<4.4", - "symfony/stopwatch": "<3.4", - "symfony/translation": "<4.4", - "symfony/twig-bridge": "<4.1.1", - "symfony/twig-bundle": "<4.4", - "symfony/validator": "<4.4", - "symfony/web-profiler-bundle": "<4.4", - "symfony/workflow": "<4.3.6" + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "phpunit/phpunit": "<5.4.3", + "symfony/asset": "<5.4", + "symfony/console": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/dom-crawler": "<5.4", + "symfony/http-client": "<5.4", + "symfony/form": "<5.4", + "symfony/lock": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/mime": "<5.4", + "symfony/property-info": "<5.4", + "symfony/property-access": "<5.4", + "symfony/serializer": "<5.4", + "symfony/security-csrf": "<5.4", + "symfony/security-core": "<5.4", + "symfony/stopwatch": "<5.4", + "symfony/translation": "<5.4", + "symfony/twig-bridge": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<5.4", + "symfony/web-profiler-bundle": "<5.4", + "symfony/workflow": "<5.4" }, "suggest": { "ext-apcu": "For best performance of the system caches", diff --git a/src/Symfony/Bundle/FrameworkBundle/phpunit.xml.dist b/src/Symfony/Bundle/FrameworkBundle/phpunit.xml.dist index c0d8df4156168..d00ee0f1e214e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/phpunit.xml.dist +++ b/src/Symfony/Bundle/FrameworkBundle/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index f46d5231851c4..ed389838e5e48 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -1,6 +1,94 @@ CHANGELOG ========= +6.0 +--- + + * The `security.authorization_checker` and `security.token_storage` services are now private + * Remove `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command, + use `UserPasswordHashCommand` and `user:hash-password` instead + * Remove the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases, + use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead + * Remove the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases, + use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead + * Remove the `logout.success_handler` and `logout.handlers` config options, register a listener on the `LogoutEvent` event instead + * Remove `FirewallConfig::getListeners()`, use `FirewallConfig::getAuthenticators()` instead + +5.4 +--- + + * Deprecate `FirewallConfig::getListeners()`, use `FirewallConfig::getAuthenticators()` instead + * Deprecate `security.authentication.basic_entry_point` and `security.authentication.retry_entry_point` services, the logic is moved into the + `HttpBasicAuthenticator` and `ChannelListener` respectively + * Deprecate `FirewallConfig::allowsAnonymous()` and the `allows_anonymous` from the data collector data, there will be no anonymous concept as of version 6. + * Deprecate not setting `$authenticatorManagerEnabled` to `true` in `SecurityDataCollector` and `DebugFirewallCommand` + * Deprecate `SecurityFactoryInterface` and `SecurityExtension::addSecurityListenerFactory()` in favor of + `AuthenticatorFactoryInterface` and `SecurityExtension::addAuthenticatorFactory()` + * Add `AuthenticatorFactoryInterface::getPriority()` which replaces `SecurityFactoryInterface::getPosition()` + * Deprecate passing an array of arrays as 1st argument to `MainConfiguration`, pass a sorted flat array of + factories instead. + * Deprecate the `always_authenticate_before_granting` option + * Display the roles of the logged-in user in the Web Debug Toolbar + * Add the `security.access_decision_manager.strategy_service` option + * Deprecate not configuring explicitly a provider for custom_authenticators when there is more than one registered provider + + +5.3 +--- + + * The authenticator system is no longer experimental + * Login Link functionality is no longer experimental + * Add `required_badges` firewall config option + * [BC break] Add `login_throttling.lock_factory` setting defaulting to `null` (instead of `lock.factory`) + * Add a `login_throttling.interval` (in `security.firewalls`) option to change the default throttling interval. + * Add the `debug:firewall` command. + * Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command, + use `UserPasswordHashCommand` and `user:hash-password` instead + * Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases, + use `security.password_hasher_factory` and `Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface` instead + * Deprecate the `security.user_password_encoder.generic` service, the `security.password_encoder` and the `Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface` aliases, + use `security.user_password_hasher`, `security.password_hasher` and `Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface` instead + * Deprecate the public `security.authorization_checker` and `security.token_storage` services to private + * Not setting the `enable_authenticator_manager` config option to `true` is deprecated + * Deprecate the `security.authentication.provider.*` services, use the new authenticator system instead + * Deprecate the `security.authentication.listener.*` services, use the new authenticator system instead + * Deprecate the Guard component integration, use the new authenticator system instead + * Add `form_login.form_only` option + +5.2.0 +----- + + * Added `FirewallListenerFactoryInterface`, which can be implemented by security factories to add firewall listeners + * Added `SortFirewallListenersPass` to make the execution order of firewall listeners configurable by + leveraging `Symfony\Component\Security\Http\Firewall\FirewallListenerInterface` + * Added ability to use comma separated ip address list for `security.access_control` + * [BC break] Removed `EntryPointFactoryInterface`, authenticators must now implement `AuthenticationEntryPointInterface` if + they require autoregistration of a Security entry point. + +5.1.0 +----- + + * Added XSD for configuration + * Added security configuration for priority-based access decision strategy + * Marked the `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, `HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory` and `X509Factory` as `@internal` + * Renamed method `AbstractFactory#createEntryPoint()` to `AbstractFactory#createDefaultEntryPoint()` + +5.0.0 +----- + + * The `switch_user.stateless` firewall option has been removed. + * Removed the ability to configure encoders using `argon2i` or `bcrypt` as algorithm, use `auto` instead + * The `simple_form` and `simple_preauth` authentication listeners have been removed, + use Guard instead. + * The `SimpleFormFactory` and `SimplePreAuthenticationFactory` classes have been removed, + use Guard instead. + * Removed `LogoutUrlHelper` and `SecurityHelper` templating helpers, use Twig instead + * Removed the `logout_on_user_change` firewall option + * Removed the `threads` encoder option + * Removed the `security.authentication.trust_resolver.anonymous_class` parameter + * Removed the `security.authentication.trust_resolver.rememberme_class` parameter + * Removed the `security.user.provider.in_memory.user` service. + 4.4.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php index 130cdfbb11e7f..a874e276feb5e 100644 --- a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php +++ b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php @@ -17,11 +17,11 @@ class ExpressionCacheWarmer implements CacheWarmerInterface { - private $expressions; + private iterable $expressions; private $expressionLanguage; /** - * @param iterable|Expression[] $expressions + * @param iterable $expressions */ public function __construct(iterable $expressions, ExpressionLanguage $expressionLanguage) { @@ -29,15 +29,20 @@ public function __construct(iterable $expressions, ExpressionLanguage $expressio $this->expressionLanguage = $expressionLanguage; } - public function isOptional() + public function isOptional(): bool { return true; } - public function warmUp($cacheDir) + /** + * @return string[] + */ + public function warmUp(string $cacheDir): array { foreach ($this->expressions as $expression) { - $this->expressionLanguage->parse($expression, ['token', 'user', 'object', 'subject', 'roles', 'role_names', 'request', 'trust_resolver']); + $this->expressionLanguage->parse($expression, ['token', 'user', 'object', 'subject', 'role_names', 'request', 'trust_resolver']); } + + return []; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php new file mode 100644 index 0000000000000..d8778328092b3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php @@ -0,0 +1,279 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Command; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; + +/** + * @author Timo Bakx + */ +#[AsCommand(name: 'debug:firewall', description: 'Display information about your security firewall(s)')] +final class DebugFirewallCommand extends Command +{ + private array $firewallNames; + private $contexts; + private $eventDispatchers; + private array $authenticators; + + /** + * @param string[] $firewallNames + * @param AuthenticatorInterface[][] $authenticators + */ + public function __construct(array $firewallNames, ContainerInterface $contexts, ContainerInterface $eventDispatchers, array $authenticators) + { + $this->firewallNames = $firewallNames; + $this->contexts = $contexts; + $this->eventDispatchers = $eventDispatchers; + $this->authenticators = $authenticators; + + parent::__construct(); + } + + protected function configure(): void + { + $exampleName = $this->getExampleName(); + + $this + ->setHelp(<<%command.name% command displays the firewalls that are configured +in your application: + + php %command.full_name% + +You can pass a firewall name to display more detailed information about +a specific firewall: + + php %command.full_name% $exampleName + +To include all events and event listeners for a specific firewall, use the +events option: + + php %command.full_name% --events $exampleName + +EOF + ) + ->setDefinition([ + new InputArgument('name', InputArgument::OPTIONAL, sprintf('A firewall name (for example "%s")', $exampleName)), + new InputOption('events', null, InputOption::VALUE_NONE, 'Include a list of event listeners (only available in combination with the "name" argument)'), + ]); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $name = $input->getArgument('name'); + + if (null === $name) { + $this->displayFirewallList($io); + + return 0; + } + + $serviceId = sprintf('security.firewall.map.context.%s', $name); + + if (!$this->contexts->has($serviceId)) { + $io->error(sprintf('Firewall %s was not found. Available firewalls are: %s', $name, implode(', ', $this->firewallNames))); + + return 1; + } + + /** @var FirewallContext $context */ + $context = $this->contexts->get($serviceId); + + $io->title(sprintf('Firewall "%s"', $name)); + + $this->displayFirewallSummary($name, $context, $io); + + $this->displaySwitchUser($context, $io); + + if ($input->getOption('events')) { + $this->displayEventListeners($name, $context, $io); + } + + $this->displayAuthenticators($name, $io); + + return 0; + } + + protected function displayFirewallList(SymfonyStyle $io): void + { + $io->title('Firewalls'); + $io->text('The following firewalls are defined:'); + + $io->listing($this->firewallNames); + + $io->comment(sprintf('To view details of a specific firewall, re-run this command with a firewall name. (e.g. debug:firewall %s)', $this->getExampleName())); + } + + protected function displayFirewallSummary(string $name, FirewallContext $context, SymfonyStyle $io): void + { + if (null === $context->getConfig()) { + return; + } + + $rows = [ + ['Name', $name], + ['Context', $context->getConfig()->getContext()], + ['Lazy', $context instanceof LazyFirewallContext ? 'Yes' : 'No'], + ['Stateless', $context->getConfig()->isStateless() ? 'Yes' : 'No'], + ['User Checker', $context->getConfig()->getUserChecker()], + ['Provider', $context->getConfig()->getProvider()], + ['Entry Point', $context->getConfig()->getEntryPoint()], + ['Access Denied URL', $context->getConfig()->getAccessDeniedUrl()], + ['Access Denied Handler', $context->getConfig()->getAccessDeniedHandler()], + ]; + + $io->table( + ['Option', 'Value'], + $rows + ); + } + + private function displaySwitchUser(FirewallContext $context, SymfonyStyle $io) + { + if ((null === $config = $context->getConfig()) || (null === $switchUser = $config->getSwitchUser())) { + return; + } + + $io->section('User switching'); + + $io->table(['Option', 'Value'], [ + ['Parameter', $switchUser['parameter'] ?? ''], + ['Provider', $switchUser['provider'] ?? $config->getProvider()], + ['User Role', $switchUser['role'] ?? ''], + ]); + } + + protected function displayEventListeners(string $name, FirewallContext $context, SymfonyStyle $io): void + { + $io->title(sprintf('Event listeners for firewall "%s"', $name)); + + $dispatcherId = sprintf('security.event_dispatcher.%s', $name); + + if (!$this->eventDispatchers->has($dispatcherId)) { + $io->text('No event dispatcher has been registered for this firewall.'); + + return; + } + + /** @var EventDispatcherInterface $dispatcher */ + $dispatcher = $this->eventDispatchers->get($dispatcherId); + + foreach ($dispatcher->getListeners() as $event => $listeners) { + $io->section(sprintf('"%s" event', $event)); + + $rows = []; + foreach ($listeners as $order => $listener) { + $rows[] = [ + sprintf('#%d', $order + 1), + $this->formatCallable($listener), + $dispatcher->getListenerPriority($event, $listener), + ]; + } + + $io->table( + ['Order', 'Callable', 'Priority'], + $rows + ); + } + } + + private function displayAuthenticators(string $name, SymfonyStyle $io): void + { + $io->title(sprintf('Authenticators for firewall "%s"', $name)); + + $authenticators = $this->authenticators[$name] ?? []; + + if (0 === \count($authenticators)) { + $io->text('No authenticators have been registered for this firewall.'); + + return; + } + + $io->table( + ['Classname'], + array_map( + static function ($authenticator) { + return [ + \get_class($authenticator), + ]; + }, + $authenticators + ) + ); + } + + private function formatCallable(mixed $callable): string + { + if (\is_array($callable)) { + if (\is_object($callable[0])) { + return sprintf('%s::%s()', \get_class($callable[0]), $callable[1]); + } + + return sprintf('%s::%s()', $callable[0], $callable[1]); + } + + if (\is_string($callable)) { + return sprintf('%s()', $callable); + } + + if ($callable instanceof \Closure) { + $r = new \ReflectionFunction($callable); + if (false !== strpos($r->name, '{closure}')) { + return 'Closure()'; + } + if ($class = $r->getClosureScopeClass()) { + return sprintf('%s::%s()', $class->name, $r->name); + } + + return $r->name.'()'; + } + + if (method_exists($callable, '__invoke')) { + return sprintf('%s::__invoke()', \get_class($callable)); + } + + throw new \InvalidArgumentException('Callable is not describable.'); + } + + private function getExampleName(): string + { + $name = 'main'; + + if (!\in_array($name, $this->firewallNames, true)) { + $name = reset($this->firewallNames); + } + + return $name; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->firewallNames); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php deleted file mode 100644 index b13ebbf33294c..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ /dev/null @@ -1,210 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Command; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Exception\InvalidArgumentException; -use Symfony\Component\Console\Exception\RuntimeException; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\Encoder\SelfSaltingEncoderInterface; - -/** - * Encode a user's password. - * - * @author Sarah Khalil - * - * @final - */ -class UserPasswordEncoderCommand extends Command -{ - protected static $defaultName = 'security:encode-password'; - - private $encoderFactory; - private $userClasses; - - public function __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = []) - { - $this->encoderFactory = $encoderFactory; - $this->userClasses = $userClasses; - - parent::__construct(); - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setDescription('Encode a password.') - ->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.') - ->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.') - ->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.') - ->setHelp(<<%command.name% command encodes passwords according to your -security configuration. This command is mainly used to generate passwords for -the in_memory user provider type and for changing passwords -in the database while developing the application. - -Suppose that you have the following security configuration in your application: - - -# app/config/security.yml -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - App\Entity\User: auto - - -If you execute the command non-interactively, the first available configured -user class under the security.encoders key is used and a random salt is -generated to encode the password: - - php %command.full_name% --no-interaction [password] - -Pass the full user class path as the second argument to encode passwords for -your own entities: - - php %command.full_name% --no-interaction [password] 'App\Entity\User' - -Executing the command interactively allows you to generate a random salt for -encoding the password: - - php %command.full_name% [password] 'App\Entity\User' - -In case your encoder doesn't require a salt, add the empty-salt option: - - php %command.full_name% --empty-salt [password] 'App\Entity\User' - -EOF - ) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io; - - $input->isInteractive() ? $errorIo->title('Symfony Password Encoder Utility') : $errorIo->newLine(); - - $password = $input->getArgument('password'); - $userClass = $this->getUserClass($input, $io); - $emptySalt = $input->getOption('empty-salt'); - - $encoder = $this->encoderFactory->getEncoder($userClass); - $saltlessWithoutEmptySalt = !$emptySalt && $encoder instanceof SelfSaltingEncoderInterface; - - if ($saltlessWithoutEmptySalt) { - $emptySalt = true; - } - - if (!$password) { - if (!$input->isInteractive()) { - $errorIo->error('The password must not be empty.'); - - return 1; - } - $passwordQuestion = $this->createPasswordQuestion(); - $password = $errorIo->askQuestion($passwordQuestion); - } - - $salt = null; - - if ($input->isInteractive() && !$emptySalt) { - $emptySalt = true; - - $errorIo->note('The command will take care of generating a salt for you. Be aware that some encoders advise to let them generate their own salt. If you\'re using one of those encoders, please answer \'no\' to the question below. '.\PHP_EOL.'Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'); - - if ($errorIo->confirm('Confirm salt generation ?')) { - $salt = $this->generateSalt(); - $emptySalt = false; - } - } elseif (!$emptySalt) { - $salt = $this->generateSalt(); - } - - $encodedPassword = $encoder->encodePassword($password, $salt); - - $rows = [ - ['Encoder used', \get_class($encoder)], - ['Encoded password', $encodedPassword], - ]; - if (!$emptySalt) { - $rows[] = ['Generated salt', $salt]; - } - $io->table(['Key', 'Value'], $rows); - - if (!$emptySalt) { - $errorIo->note(sprintf('Make sure that your salt storage field fits the salt length: %s chars', \strlen($salt))); - } elseif ($saltlessWithoutEmptySalt) { - $errorIo->note('Self-salting encoder used: the encoder generated its own built-in salt.'); - } - - $errorIo->success('Password encoding succeeded'); - - return 0; - } - - /** - * Create the password question to ask the user for the password to be encoded. - */ - private function createPasswordQuestion(): Question - { - $passwordQuestion = new Question('Type in your password to be encoded'); - - return $passwordQuestion->setValidator(function ($value) { - if ('' === trim($value)) { - throw new InvalidArgumentException('The password must not be empty.'); - } - - return $value; - })->setHidden(true)->setMaxAttempts(20); - } - - private function generateSalt(): string - { - return base64_encode(random_bytes(30)); - } - - private function getUserClass(InputInterface $input, SymfonyStyle $io): string - { - if (null !== $userClass = $input->getArgument('user-class')) { - return $userClass; - } - - if (empty($this->userClasses)) { - throw new RuntimeException('There are no configured encoders for the "security" extension.'); - } - - if (!$input->isInteractive() || 1 === \count($this->userClasses)) { - return reset($this->userClasses); - } - - $userClasses = $this->userClasses; - natcasesort($userClasses); - $userClasses = array_values($userClasses); - - return $io->choice('For which user class would you like to encode a password?', $userClasses, reset($userClasses)); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index e5756bb0060a3..0fdee87821861 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -17,15 +17,12 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; -use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter; -use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; -use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Http\Firewall\SwitchUserListener; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; @@ -35,7 +32,7 @@ /** * @author Fabien Potencier * - * @final since Symfony 4.4 + * @final */ class SecurityDataCollector extends DataCollector implements LateDataCollectorInterface { @@ -45,7 +42,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn private $accessDecisionManager; private $firewallMap; private $firewall; - private $hasVarDumper; + private bool $hasVarDumper; public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null, FirewallMapInterface $firewallMap = null, TraceableFirewallListener $firewall = null) { @@ -60,10 +57,8 @@ public function __construct(TokenStorageInterface $tokenStorage = null, RoleHier /** * {@inheritdoc} - * - * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response/*, \Throwable $exception = null*/) + public function collect(Request $request, Response $response, \Throwable $exception = null) { if (null === $this->tokenStorage) { $this->data = [ @@ -97,33 +92,16 @@ public function collect(Request $request, Response $response/*, \Throwable $exce ]; } else { $inheritedRoles = []; - - if (method_exists($token, 'getRoleNames')) { - $assignedRoles = $token->getRoleNames(); - } else { - $assignedRoles = array_map(function (Role $role) { return $role->getRole(); }, $token->getRoles(false)); - } + $assignedRoles = $token->getRoleNames(); $impersonatorUser = null; if ($token instanceof SwitchUserToken) { - $impersonatorUser = $token->getOriginalToken()->getUsername(); - } else { - foreach ($token->getRoles(false) as $role) { - if ($role instanceof SwitchUserRole) { - $impersonatorUser = $role->getSource()->getUsername(); - break; - } - } + $originalToken = $token->getOriginalToken(); + $impersonatorUser = $originalToken->getUserIdentifier(); } if (null !== $this->roleHierarchy) { - if (method_exists($this->roleHierarchy, 'getReachableRoleNames')) { - $allRoles = $this->roleHierarchy->getReachableRoleNames($assignedRoles); - } else { - $allRoles = array_map(function (Role $role) { return (string) $role; }, $this->roleHierarchy->getReachableRoles($token->getRoles(false))); - } - - foreach ($allRoles as $role) { + foreach ($this->roleHierarchy->getReachableRoleNames($assignedRoles) as $role) { if (!\in_array($role, $assignedRoles, true)) { $inheritedRoles[] = $role; } @@ -132,7 +110,7 @@ public function collect(Request $request, Response $response/*, \Throwable $exce $logoutUrl = null; try { - if (null !== $this->logoutUrlGenerator && !$token instanceof AnonymousToken) { + if (null !== $this->logoutUrlGenerator) { $logoutUrl = $this->logoutUrlGenerator->getLogoutPath(); } } catch (\Exception $e) { @@ -141,14 +119,14 @@ public function collect(Request $request, Response $response/*, \Throwable $exce $this->data = [ 'enabled' => true, - 'authenticated' => $token->isAuthenticated(), + 'authenticated' => method_exists($token, 'isAuthenticated') ? $token->isAuthenticated(false) : (bool) $token->getUser(), 'impersonated' => null !== $impersonatorUser, 'impersonator_user' => $impersonatorUser, 'impersonation_exit_path' => null, 'token' => $token, 'token_class' => $this->hasVarDumper ? new ClassStub(\get_class($token)) : \get_class($token), 'logout_url' => $logoutUrl, - 'user' => $token->getUsername(), + 'user' => $token->getUserIdentifier(), 'roles' => $assignedRoles, 'inherited_roles' => array_unique($inheritedRoles), 'supports_role_hierarchy' => null !== $this->roleHierarchy, @@ -197,7 +175,6 @@ public function collect(Request $request, Response $response/*, \Throwable $exce if (null !== $firewallConfig) { $this->data['firewall'] = [ 'name' => $firewallConfig->getName(), - 'allows_anonymous' => $firewallConfig->allowsAnonymous(), 'request_matcher' => $firewallConfig->getRequestMatcher(), 'security_enabled' => $firewallConfig->isSecurityEnabled(), 'stateless' => $firewallConfig->isStateless(), @@ -207,7 +184,7 @@ public function collect(Request $request, Response $response/*, \Throwable $exce 'access_denied_handler' => $firewallConfig->getAccessDeniedHandler(), 'access_denied_url' => $firewallConfig->getAccessDeniedUrl(), 'user_checker' => $firewallConfig->getUserChecker(), - 'listeners' => $firewallConfig->getListeners(), + 'authenticators' => $firewallConfig->getAuthenticators(), ]; // generate exit impersonation path from current request @@ -226,6 +203,8 @@ public function collect(Request $request, Response $response/*, \Throwable $exce if ($this->firewall) { $this->data['listeners'] = $this->firewall->getWrappedListeners(); } + + $this->data['authenticators'] = $this->firewall ? $this->firewall->getAuthenticatorsInfo() : []; } /** @@ -243,40 +222,32 @@ public function lateCollect() /** * Checks if security is enabled. - * - * @return bool true if security is enabled, false otherwise */ - public function isEnabled() + public function isEnabled(): bool { return $this->data['enabled']; } /** * Gets the user. - * - * @return string The user */ - public function getUser() + public function getUser(): string { return $this->data['user']; } /** * Gets the roles of the user. - * - * @return array|Data */ - public function getRoles() + public function getRoles(): array|Data { return $this->data['roles']; } /** * Gets the inherited roles of the user. - * - * @return array|Data */ - public function getInheritedRoles() + public function getInheritedRoles(): array|Data { return $this->data['inherited_roles']; } @@ -284,74 +255,55 @@ public function getInheritedRoles() /** * Checks if the data contains information about inherited roles. Still the inherited * roles can be an empty array. - * - * @return bool true if the profile was contains inherited role information */ - public function supportsRoleHierarchy() + public function supportsRoleHierarchy(): bool { return $this->data['supports_role_hierarchy']; } /** * Checks if the user is authenticated or not. - * - * @return bool true if the user is authenticated, false otherwise */ - public function isAuthenticated() + public function isAuthenticated(): bool { return $this->data['authenticated']; } - /** - * @return bool - */ - public function isImpersonated() + public function isImpersonated(): bool { return $this->data['impersonated']; } - /** - * @return string|null - */ - public function getImpersonatorUser() + public function getImpersonatorUser(): ?string { return $this->data['impersonator_user']; } - /** - * @return string|null - */ - public function getImpersonationExitPath() + public function getImpersonationExitPath(): ?string { return $this->data['impersonation_exit_path']; } /** * Get the class name of the security token. - * - * @return string|Data|null The token */ - public function getTokenClass() + public function getTokenClass(): string|Data|null { return $this->data['token_class']; } /** * Get the full security token class as Data object. - * - * @return Data|null */ - public function getToken() + public function getToken(): ?Data { return $this->data['token']; } /** * Get the logout URL. - * - * @return string|null The logout URL */ - public function getLogoutUrl() + public function getLogoutUrl(): ?string { return $this->data['logout_url']; } @@ -361,53 +313,49 @@ public function getLogoutUrl() * * @return string[]|Data */ - public function getVoters() + public function getVoters(): array|Data { return $this->data['voters']; } /** * Returns the strategy configured for the security voters. - * - * @return string */ - public function getVoterStrategy() + public function getVoterStrategy(): string { return $this->data['voter_strategy']; } /** * Returns the log of the security decisions made by the access decision manager. - * - * @return array|Data */ - public function getAccessDecisionLog() + public function getAccessDecisionLog(): array|Data { return $this->data['access_decision_log']; } /** * Returns the configuration of the current firewall context. - * - * @return array|Data|null */ - public function getFirewall() + public function getFirewall(): array|Data|null { return $this->data['firewall']; } - /** - * @return array|Data - */ - public function getListeners() + public function getListeners(): array|Data { return $this->data['listeners']; } + public function getAuthenticators(): array|Data + { + return $this->data['authenticators']; + } + /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'security'; } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php index e7f9df1221e69..45eae8f605202 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php @@ -15,33 +15,44 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallContext; use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Http\Firewall\AbstractListener; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; /** - * Firewall collecting called listeners. + * Firewall collecting called security listeners and authenticators. * * @author Robin Chalas */ final class TraceableFirewallListener extends FirewallListener { - private $wrappedListeners = []; + private array $wrappedListeners = []; + private array $authenticatorsInfo = []; public function getWrappedListeners() { return $this->wrappedListeners; } + public function getAuthenticatorsInfo(): array + { + return $this->authenticatorsInfo; + } + protected function callListeners(RequestEvent $event, iterable $listeners) { $wrappedListeners = []; $wrappedLazyListeners = []; + $authenticatorManagerListener = null; foreach ($listeners as $listener) { if ($listener instanceof LazyFirewallContext) { - \Closure::bind(function () use (&$wrappedLazyListeners, &$wrappedListeners) { + \Closure::bind(function () use (&$wrappedLazyListeners, &$wrappedListeners, &$authenticatorManagerListener) { $listeners = []; foreach ($this->listeners as $listener) { - if ($listener instanceof AbstractListener) { + if (!$authenticatorManagerListener && $listener instanceof TraceableAuthenticatorManagerListener) { + $authenticatorManagerListener = $listener; + } + if ($listener instanceof FirewallListenerInterface) { $listener = new WrappedLazyListener($listener); $listeners[] = $listener; $wrappedLazyListeners[] = $listener; @@ -58,9 +69,12 @@ protected function callListeners(RequestEvent $event, iterable $listeners) $listener($event); } else { - $wrappedListener = $listener instanceof AbstractListener ? new WrappedLazyListener($listener) : new WrappedListener($listener); + $wrappedListener = $listener instanceof FirewallListenerInterface ? new WrappedLazyListener($listener) : new WrappedListener($listener); $wrappedListener($event); $wrappedListeners[] = $wrappedListener->getInfo(); + if (!$authenticatorManagerListener && $listener instanceof TraceableAuthenticatorManagerListener) { + $authenticatorManagerListener = $listener; + } } if ($event->hasResponse()) { @@ -75,5 +89,9 @@ protected function callListeners(RequestEvent $event, iterable $listeners) } $this->wrappedListeners = array_merge($this->wrappedListeners, $wrappedListeners); + + if ($authenticatorManagerListener) { + $this->authenticatorsInfo = $authenticatorManagerListener->getAuthenticatorsInfo(); + } } } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php index b758be6242660..ed2f8e7144f02 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Debug; -use Symfony\Component\Security\Http\Firewall\LegacyListenerTrait; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; use Symfony\Component\VarDumper\Caster\ClassStub; /** @@ -21,12 +21,10 @@ */ trait TraceableListenerTrait { - use LegacyListenerTrait; - - private $response; - private $listener; - private $time; - private $stub; + private $response = null; + private mixed $listener; + private ?float $time = null; + private object $stub; /** * Proxies all method calls to the original listener. @@ -46,7 +44,7 @@ public function getInfo(): array return [ 'response' => $this->response, 'time' => $this->time, - 'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener), + 'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener instanceof TraceableAuthenticatorManagerListener ? $this->listener->getAuthenticatorManagerListener() : $this->listener), ]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php index eca63b4d4a2fc..5a3d0a1c609d8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php @@ -15,7 +15,7 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Exception\LazyResponseException; use Symfony\Component\Security\Http\Firewall\AbstractListener; -use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; /** * Wraps a lazy security listener. @@ -24,11 +24,11 @@ * * @internal */ -final class WrappedLazyListener extends AbstractListener implements ListenerInterface +final class WrappedLazyListener extends AbstractListener { use TraceableListenerTrait; - public function __construct(AbstractListener $listener) + public function __construct(FirewallListenerInterface $listener) { $this->listener = $listener; } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php index 8404c73b0f577..bce3d265560a5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php @@ -12,40 +12,27 @@ namespace Symfony\Bundle\SecurityBundle\Debug; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Http\Firewall\AbstractListener; -use Symfony\Component\Security\Http\Firewall\ListenerInterface; /** * Wraps a security listener for calls record. * * @author Robin Chalas * - * @internal since Symfony 4.3 + * @internal */ -final class WrappedListener implements ListenerInterface +final class WrappedListener { use TraceableListenerTrait; - /** - * @param callable $listener - */ - public function __construct($listener) + public function __construct(callable $listener) { $this->listener = $listener; } - /** - * {@inheritdoc} - */ public function __invoke(RequestEvent $event) { $startTime = microtime(true); - if (\is_callable($this->listener)) { - ($this->listener)($event); - } else { - @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($this->listener), AbstractListener::class), \E_USER_DEPRECATED); - $this->listener->handle($event); - } + ($this->listener)($event); $this->time = microtime(true) - $startTime; $this->response = $event->getResponse(); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/CleanRememberMeVerifierPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/CleanRememberMeVerifierPass.php new file mode 100644 index 0000000000000..d959d4bda9e67 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/CleanRememberMeVerifierPass.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Cleans up the remember me verifier cache if cache is missing. + * + * @author Jordi Boggiano + */ +class CleanRememberMeVerifierPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('cache.system')) { + $container->removeDefinition('cache.security_token_verifier'); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php new file mode 100644 index 0000000000000..ccab76679a32a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfFeaturesPass.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener; +use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener; + +/** + * @author Christian Flothmann + * @author Wouter de Jong + * + * @internal + */ +class RegisterCsrfFeaturesPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $this->registerCsrfProtectionListener($container); + $this->registerLogoutHandler($container); + } + + private function registerCsrfProtectionListener(ContainerBuilder $container) + { + if (!$container->has('security.authenticator.manager') || !$container->has('security.csrf.token_manager')) { + return; + } + + $container->register('security.listener.csrf_protection', CsrfProtectionListener::class) + ->addArgument(new Reference('security.csrf.token_manager')) + ->addTag('kernel.event_subscriber') + ->setPublic(false); + } + + protected function registerLogoutHandler(ContainerBuilder $container) + { + if (!$container->has('security.logout_listener') || !$container->has('security.csrf.token_storage')) { + return; + } + + $csrfTokenStorage = $container->findDefinition('security.csrf.token_storage'); + $csrfTokenStorageClass = $container->getParameterBag()->resolveValue($csrfTokenStorage->getClass()); + + if (!is_subclass_of($csrfTokenStorageClass, 'Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface')) { + return; + } + + $container->register('security.logout.listener.csrf_token_clearing', CsrfTokenClearingLogoutListener::class) + ->addArgument(new Reference('security.csrf.token_storage')) + ->addTag('kernel.event_subscriber') + ->setPublic(false); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php deleted file mode 100644 index 0d7527c26bb79..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterCsrfTokenClearingLogoutHandlerPass.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @author Christian Flothmann - */ -class RegisterCsrfTokenClearingLogoutHandlerPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container) - { - if (!$container->has('security.logout_listener') || !$container->has('security.csrf.token_storage')) { - return; - } - - $csrfTokenStorage = $container->findDefinition('security.csrf.token_storage'); - $csrfTokenStorageClass = $container->getParameterBag()->resolveValue($csrfTokenStorage->getClass()); - - if (!is_subclass_of($csrfTokenStorageClass, 'Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface')) { - return; - } - - $container->register('security.logout.handler.csrf_token_clearing', 'Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler') - ->addArgument(new Reference('security.csrf.token_storage')) - ->setPublic(false); - - $container->findDefinition('security.logout_listener')->addMethodCall('addHandler', [new Reference('security.logout.handler.csrf_token_clearing')]); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterEntryPointPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterEntryPointPass.php new file mode 100644 index 0000000000000..6de49517f3405 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterEntryPointPass.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + +/** + * @author Wouter de Jong + */ +class RegisterEntryPointPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasParameter('security.firewalls')) { + return; + } + + $firewalls = $container->getParameter('security.firewalls'); + foreach ($firewalls as $firewallName) { + if (!$container->hasDefinition('security.authenticator.manager.'.$firewallName) || !$container->hasParameter('security.'.$firewallName.'._indexed_authenticators')) { + continue; + } + + $entryPoints = []; + $indexedAuthenticators = $container->getParameter('security.'.$firewallName.'._indexed_authenticators'); + // this is a compile-only parameter, removing it cleans up space and avoids unintended usage + $container->getParameterBag()->remove('security.'.$firewallName.'._indexed_authenticators'); + foreach ($indexedAuthenticators as $key => $authenticatorId) { + if (!$container->has($authenticatorId)) { + continue; + } + + // because this pass runs before ResolveChildDefinitionPass, child definitions didn't inherit the parent class yet + $definition = $container->findDefinition($authenticatorId); + while (!($authenticatorClass = $definition->getClass()) && $definition instanceof ChildDefinition) { + $definition = $container->findDefinition($definition->getParent()); + } + + if (is_a($authenticatorClass, AuthenticationEntryPointInterface::class, true)) { + $entryPoints[$key] = $authenticatorId; + } + } + + if (!$entryPoints) { + continue; + } + + $config = $container->getDefinition('security.firewall.map.config.'.$firewallName); + $configuredEntryPoint = $config->getArgument(7); + + if (null !== $configuredEntryPoint) { + // allow entry points to be configured by authenticator key (e.g. "http_basic") + $entryPoint = $entryPoints[$configuredEntryPoint] ?? $configuredEntryPoint; + } elseif (1 === \count($entryPoints)) { + $entryPoint = array_shift($entryPoints); + } else { + $entryPointNames = []; + foreach ($entryPoints as $key => $serviceId) { + $entryPointNames[] = is_numeric($key) ? $serviceId : $key; + } + + throw new InvalidConfigurationException(sprintf('Because you have multiple authenticators in firewall "%s", you need to set the "entry_point" key to one of your authenticators ("%s") or a service ID implementing "%s". The "entry_point" determines what should happen (e.g. redirect to "/login") when an anonymous user tries to access a protected page.', $firewallName, implode('", "', $entryPointNames), AuthenticationEntryPointInterface::class)); + } + + $config->replaceArgument(7, $entryPoint); + $container->getDefinition('security.exception_listener.'.$firewallName)->replaceArgument(4, new Reference($entryPoint)); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPass.php new file mode 100644 index 0000000000000..20094957cdb65 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPass.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent; +use Symfony\Component\Security\Http\SecurityEvents; + +/** + * Makes sure all event listeners on the global dispatcher are also listening + * to events on the firewall-specific dispatchers. + * + * This compiler pass must be run after RegisterListenersPass of the + * EventDispatcher component. + * + * @author Wouter de Jong + * + * @internal + */ +class RegisterGlobalSecurityEventListenersPass implements CompilerPassInterface +{ + private const EVENT_BUBBLING_EVENTS = [ + CheckPassportEvent::class, + LoginFailureEvent::class, + LoginSuccessEvent::class, + LogoutEvent::class, + AuthenticationTokenCreatedEvent::class, + AuthenticationSuccessEvent::class, + InteractiveLoginEvent::class, + TokenDeauthenticatedEvent::class, + + // When events are registered by their name + AuthenticationEvents::AUTHENTICATION_SUCCESS, + SecurityEvents::INTERACTIVE_LOGIN, + ]; + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->has('event_dispatcher') || !$container->hasParameter('security.firewalls')) { + return; + } + + $firewallDispatchers = []; + foreach ($container->getParameter('security.firewalls') as $firewallName) { + if (!$container->has('security.event_dispatcher.'.$firewallName)) { + continue; + } + + $firewallDispatchers[] = $container->findDefinition('security.event_dispatcher.'.$firewallName); + } + + $globalDispatcher = $container->findDefinition('event_dispatcher'); + foreach ($globalDispatcher->getMethodCalls() as $methodCall) { + if ('addListener' !== $methodCall[0]) { + continue; + } + + $methodCallArguments = $methodCall[1]; + if (!\in_array($methodCallArguments[0], self::EVENT_BUBBLING_EVENTS, true)) { + continue; + } + + foreach ($firewallDispatchers as $firewallDispatcher) { + $firewallDispatcher->addMethodCall('addListener', $methodCallArguments); + } + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterLdapLocatorPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterLdapLocatorPass.php new file mode 100644 index 0000000000000..295f363292245 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterLdapLocatorPass.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @author Wouter de Jong + * + * @internal + */ +class RegisterLdapLocatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $definition = $container->setDefinition('security.ldap_locator', new Definition(ServiceLocator::class)); + + $locators = []; + foreach ($container->findTaggedServiceIds('ldap') as $serviceId => $tags) { + $locators[$serviceId] = new ServiceClosureArgument(new Reference($serviceId)); + } + + $definition->addArgument($locators); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php index 6aa50f4196c8b..2b1159eff979a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php @@ -41,7 +41,7 @@ public function process(ContainerBuilder $container) TokenStorageInterface::class => new BoundArgument(new Reference('security.untracked_token_storage'), false), ]); - if (!$container->has('session')) { + if (!$container->has('session.factory')) { $container->setAlias('security.token_storage', 'security.untracked_token_storage')->setPublic(true); $container->getDefinition('security.untracked_token_storage')->addTag('kernel.reset', ['method' => 'reset']); } elseif ($container->hasDefinition('security.context_listener')) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/ReplaceDecoratedRememberMeHandlerPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/ReplaceDecoratedRememberMeHandlerPass.php new file mode 100644 index 0000000000000..5de431c2c04c8 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/ReplaceDecoratedRememberMeHandlerPass.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Replaces the DecoratedRememberMeHandler services with the real definition. + * + * @author Wouter de Jong + * + * @internal + */ +final class ReplaceDecoratedRememberMeHandlerPass implements CompilerPassInterface +{ + private const HANDLER_TAG = 'security.remember_me_handler'; + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container): void + { + $handledFirewalls = []; + foreach ($container->findTaggedServiceIds(self::HANDLER_TAG) as $definitionId => $rememberMeHandlerTags) { + $definition = $container->findDefinition($definitionId); + if (DecoratedRememberMeHandler::class !== $definition->getClass()) { + continue; + } + + // get the actual custom remember me handler definition (passed to the decorator) + $realRememberMeHandler = $container->findDefinition((string) $definition->getArgument(0)); + if (null === $realRememberMeHandler) { + throw new \LogicException(sprintf('Invalid service definition for custom remember me handler; no service found with ID "%s".', (string) $definition->getArgument(0))); + } + + foreach ($rememberMeHandlerTags as $rememberMeHandlerTag) { + // some custom handlers may be used on multiple firewalls in the same application + if (\in_array($rememberMeHandlerTag['firewall'], $handledFirewalls, true)) { + continue; + } + + $rememberMeHandler = clone $realRememberMeHandler; + $rememberMeHandler->addTag(self::HANDLER_TAG, $rememberMeHandlerTag); + $container->setDefinition('security.authenticator.remember_me_handler.'.$rememberMeHandlerTag['firewall'], $rememberMeHandler); + + $handledFirewalls[] = $rememberMeHandlerTag['firewall']; + } + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/SortFirewallListenersPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/SortFirewallListenersPass.php new file mode 100644 index 0000000000000..6d49320445c10 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/SortFirewallListenersPass.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; + +/** + * Sorts firewall listeners based on the execution order provided by FirewallListenerInterface::getPriority(). + * + * @author Christian Scheb + */ +class SortFirewallListenersPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasParameter('security.firewalls')) { + return; + } + + foreach ($container->getParameter('security.firewalls') as $firewallName) { + $firewallContextDefinition = $container->getDefinition('security.firewall.map.context.'.$firewallName); + $this->sortFirewallContextListeners($firewallContextDefinition, $container); + } + } + + private function sortFirewallContextListeners(Definition $definition, ContainerBuilder $container): void + { + /** @var IteratorArgument $listenerIteratorArgument */ + $listenerIteratorArgument = $definition->getArgument(0); + $prioritiesByServiceId = $this->getListenerPriorities($listenerIteratorArgument, $container); + + $listeners = $listenerIteratorArgument->getValues(); + usort($listeners, function (Reference $a, Reference $b) use ($prioritiesByServiceId) { + return $prioritiesByServiceId[(string) $b] <=> $prioritiesByServiceId[(string) $a]; + }); + + $listenerIteratorArgument->setValues(array_values($listeners)); + } + + private function getListenerPriorities(IteratorArgument $listeners, ContainerBuilder $container): array + { + $priorities = []; + + foreach ($listeners->getValues() as $reference) { + $id = (string) $reference; + $def = $container->getDefinition($id); + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + + $priority = 0; + if ($r->isSubclassOf(FirewallListenerInterface::class)) { + $priority = $r->getMethod('getPriority')->invoke(null); + } + + $priorities[$id] = $priority; + } + + return $priorities; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 2f8714aaf7221..e7dbdd42a7868 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -12,12 +12,12 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePreAuthenticationFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; /** @@ -27,9 +27,21 @@ */ class MainConfiguration implements ConfigurationInterface { - private $factories; - private $userProviderFactories; + /** @internal */ + public const STRATEGY_AFFIRMATIVE = 'affirmative'; + /** @internal */ + public const STRATEGY_CONSENSUS = 'consensus'; + /** @internal */ + public const STRATEGY_UNANIMOUS = 'unanimous'; + /** @internal */ + public const STRATEGY_PRIORITY = 'priority'; + private array $factories; + private array $userProviderFactories; + + /** + * @param array $factories + */ public function __construct(array $factories, array $userProviderFactories) { $this->factories = $factories; @@ -38,10 +50,8 @@ public function __construct(array $factories, array $userProviderFactories) /** * Generates the configuration tree builder. - * - * @return TreeBuilder The tree builder */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $tb = new TreeBuilder('security'); $rootNode = $tb->getRootNode(); @@ -54,27 +64,36 @@ public function getConfigTreeBuilder() ->defaultValue(SessionAuthenticationStrategy::MIGRATE) ->end() ->booleanNode('hide_user_not_found')->defaultTrue()->end() - ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end() ->booleanNode('erase_credentials')->defaultTrue()->end() + ->booleanNode('enable_authenticator_manager')->defaultTrue()->end() ->arrayNode('access_decision_manager') ->addDefaultsIfNotSet() ->children() ->enumNode('strategy') - ->values([AccessDecisionManager::STRATEGY_AFFIRMATIVE, AccessDecisionManager::STRATEGY_CONSENSUS, AccessDecisionManager::STRATEGY_UNANIMOUS]) + ->values($this->getAccessDecisionStrategies()) ->end() ->scalarNode('service')->end() + ->scalarNode('strategy_service')->end() ->booleanNode('allow_if_all_abstain')->defaultFalse()->end() ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end() ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['strategy']) && isset($v['service']); }) + ->ifTrue(function ($v) { return isset($v['strategy'], $v['service']); }) ->thenInvalid('"strategy" and "service" cannot be used together.') ->end() + ->validate() + ->ifTrue(function ($v) { return isset($v['strategy'], $v['strategy_service']); }) + ->thenInvalid('"strategy" and "strategy_service" cannot be used together.') + ->end() + ->validate() + ->ifTrue(function ($v) { return isset($v['service'], $v['strategy_service']); }) + ->thenInvalid('"service" and "strategy_service" cannot be used together.') + ->end() ->end() ->end() ; - $this->addEncodersSection($rootNode); + $this->addPasswordHashersSection($rootNode); $this->addProvidersSection($rootNode); $this->addFirewallsSection($rootNode, $this->factories); $this->addAccessControlSection($rootNode); @@ -146,6 +165,9 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode) ; } + /** + * @param array $factories + */ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories) { $firewallNodeBuilder = $rootNode @@ -157,6 +179,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->disallowNewKeysInSubsequentConfigs() ->useAttributeAsKey('name') ->prototype('array') + ->fixXmlConfig('required_badge') ->children() ; @@ -176,15 +199,13 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('request_matcher')->end() ->scalarNode('access_denied_url')->end() ->scalarNode('access_denied_handler')->end() - ->scalarNode('entry_point')->end() + ->scalarNode('entry_point') + ->info(sprintf('An enabled authenticator name or a service id that implements "%s"', AuthenticationEntryPointInterface::class)) + ->end() ->scalarNode('provider')->end() ->booleanNode('stateless')->defaultFalse()->end() + ->booleanNode('lazy')->defaultFalse()->end() ->scalarNode('context')->cannotBeEmpty()->end() - ->booleanNode('logout_on_user_change') - ->defaultTrue() - ->info('When true, it will trigger a logout for the user if something has changed. Note: No-Op option since 4.0. Will always be true.') - ->setDeprecated('The "%path%.%node%" configuration key has been deprecated in Symfony 4.1.') - ->end() ->arrayNode('logout') ->treatTrueLike([]) ->canBeUnset() @@ -194,7 +215,6 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('csrf_token_id')->defaultValue('logout')->end() ->scalarNode('path')->defaultValue('/logout')->end() ->scalarNode('target')->defaultValue('/')->end() - ->scalarNode('success_handler')->end() ->booleanNode('invalidate_session')->defaultTrue()->end() ->end() ->fixXmlConfig('delete_cookie') @@ -205,22 +225,6 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->ifTrue(function ($v) { return \is_array($v) && \is_int(key($v)); }) ->then(function ($v) { return array_map(function ($v) { return ['name' => $v]; }, $v); }) ->end() - ->beforeNormalization() - ->ifArray()->then(function ($v) { - foreach ($v as $originalName => $cookieConfig) { - if (str_contains($originalName, '-')) { - $normalizedName = str_replace('-', '_', $originalName); - @trigger_error(sprintf('Normalization of cookie names is deprecated since Symfony 4.3. Starting from Symfony 5.0, the "%s" cookie configured in "logout.delete_cookies" will delete the "%s" cookie instead of the "%s" cookie.', $originalName, $originalName, $normalizedName), \E_USER_DEPRECATED); - - // normalize cookie names manually for BC reasons. Remove it in Symfony 5.0. - $v[$normalizedName] = $cookieConfig; - unset($v[$originalName]); - } - } - - return $v; - }) - ->end() ->useAttributeAsKey('name') ->prototype('array') ->children() @@ -232,12 +236,6 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->end() ->end() ->end() - ->fixXmlConfig('handler') - ->children() - ->arrayNode('handlers') - ->prototype('scalar')->end() - ->end() - ->end() ->end() ->arrayNode('switch_user') ->canBeUnset() @@ -245,32 +243,45 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('provider')->end() ->scalarNode('parameter')->defaultValue('_switch_user')->end() ->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end() - ->booleanNode('stateless') - ->setDeprecated('The "%path%.%node%" configuration key has been deprecated in Symfony 4.1.') - ->defaultValue(false) - ->end() ->end() ->end() - ; + ->arrayNode('required_badges') + ->info('A list of badges that must be present on the authenticated passport.') + ->validate() + ->always() + ->then(function ($requiredBadges) { + return array_map(function ($requiredBadge) { + if (class_exists($requiredBadge)) { + return $requiredBadge; + } - $abstractFactoryKeys = []; - foreach ($factories as $factoriesAtPosition) { - foreach ($factoriesAtPosition as $factory) { - $name = str_replace('-', '_', $factory->getKey()); - $factoryNode = $firewallNodeBuilder->arrayNode($name) - ->canBeUnset() - ; + if (false === strpos($requiredBadge, '\\')) { + $fqcn = 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\\'.$requiredBadge; + if (class_exists($fqcn)) { + return $fqcn; + } + } - if ($factory instanceof SimplePreAuthenticationFactory || $factory instanceof SimpleFormFactory) { - $factoryNode->setDeprecated(sprintf('The "%s" security listener is deprecated Symfony 4.2, use Guard instead.', $name)); - } + throw new InvalidConfigurationException(sprintf('Undefined security Badge class "%s" set in "security.firewall.required_badges".', $requiredBadge)); + }, $requiredBadges); + }) + ->end() + ->prototype('scalar')->end() + ->end() + ; - if ($factory instanceof AbstractFactory) { - $abstractFactoryKeys[] = $name; - } + $abstractFactoryKeys = []; + foreach ($factories as $factory) { + $name = str_replace('-', '_', $factory->getKey()); + $factoryNode = $firewallNodeBuilder->arrayNode($name) + ->canBeUnset() + ; - $factory->addConfiguration($factoryNode); + if ($factory instanceof AbstractFactory) { + $abstractFactoryKeys[] = $name; } + + $factory->addConfiguration($factoryNode); } // check for unreachable check paths @@ -356,12 +367,12 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ; } - private function addEncodersSection(ArrayNodeDefinition $rootNode) + private function addPasswordHashersSection(ArrayNodeDefinition $rootNode) { $rootNode - ->fixXmlConfig('encoder') + ->fixXmlConfig('password_hasher') ->children() - ->arrayNode('encoders') + ->arrayNode('password_hashers') ->example([ 'App\Entity\User1' => 'auto', 'App\Entity\User2' => [ @@ -400,15 +411,20 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode) ->end() ->scalarNode('memory_cost')->defaultNull()->end() ->scalarNode('time_cost')->defaultNull()->end() - ->scalarNode('threads') - ->defaultNull() - ->setDeprecated('The "%path%.%node%" configuration key has no effect since Symfony 4.3 and will be removed in 5.0.') - ->end() ->scalarNode('id')->end() ->end() ->end() ->end() - ->end() - ; + ->end(); + } + + private function getAccessDecisionStrategies(): array + { + return [ + self::STRATEGY_AFFIRMATIVE, + self::STRATEGY_CONSENSUS, + self::STRATEGY_UNANIMOUS, + self::STRATEGY_PRIORITY, + ]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 79ef873ec2ad2..b0c6b5c0ecb79 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -24,12 +24,13 @@ * @author Lukas Kahwe Smith * @author Johannes M. Schmitt */ -abstract class AbstractFactory implements SecurityFactoryInterface +abstract class AbstractFactory implements AuthenticatorFactoryInterface { protected $options = [ 'check_path' => '/login_check', 'use_forward' => false, 'require_previous_session' => false, + 'login_path' => '/login', ]; protected $defaultSuccessHandlerOptions = [ @@ -47,26 +48,9 @@ abstract class AbstractFactory implements SecurityFactoryInterface 'failure_path_parameter' => '_failure_path', ]; - public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId) + final public function addOption(string $name, mixed $default = null): void { - // authentication provider - $authProviderId = $this->createAuthProvider($container, $id, $config, $userProviderId); - - // authentication listener - $listenerId = $this->createListener($container, $id, $config, $userProviderId); - - // add remember-me aware tag if requested - if ($this->isRememberMeAware($config)) { - $container - ->getDefinition($listenerId) - ->addTag('security.remember_me_aware', ['id' => $id, 'provider' => $userProviderId]) - ; - } - - // create entry point if applicable (optional) - $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPointId); - - return [$authProviderId, $listenerId, $entryPointId]; + $this->options[$name] = $default; } public function addConfiguration(NodeDefinition $node) @@ -89,83 +73,7 @@ public function addConfiguration(NodeDefinition $node) } } - final public function addOption(string $name, $default = null) - { - $this->options[$name] = $default; - } - - /** - * Subclasses must return the id of a service which implements the - * AuthenticationProviderInterface. - * - * @param string $id The unique id of the firewall - * @param array $config The options array for this listener - * @param string $userProviderId The id of the user provider - * - * @return string never null, the id of the authentication provider - */ - abstract protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId); - - /** - * Subclasses must return the id of the abstract listener template. - * - * Listener definitions should inherit from the AbstractAuthenticationListener - * like this: - * - * - * - * In the above case, this method would return "my.listener.id". - * - * @return string - */ - abstract protected function getListenerId(); - - /** - * Subclasses may create an entry point of their as they see fit. The - * default implementation does not change the default entry point. - * - * @param ContainerBuilder $container - * @param string $id - * @param array $config - * @param string|null $defaultEntryPointId - * - * @return string|null the entry point id - */ - protected function createEntryPoint($container, $id, $config, $defaultEntryPointId) - { - return $defaultEntryPointId; - } - - /** - * Subclasses may disable remember-me features for the listener, by - * always returning false from this method. - * - * @return bool Whether a possibly configured RememberMeServices should be set for this listener - */ - protected function isRememberMeAware($config) - { - return $config['remember_me']; - } - - protected function createListener($container, $id, $config, $userProvider) - { - $listenerId = $this->getListenerId(); - $listener = new ChildDefinition($listenerId); - $listener->replaceArgument(4, $id); - $listener->replaceArgument(5, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config))); - $listener->replaceArgument(6, new Reference($this->createAuthenticationFailureHandler($container, $id, $config))); - $listener->replaceArgument(7, array_intersect_key($config, $this->options)); - - $listenerId .= '.'.$id; - $container->setDefinition($listenerId, $listener); - - return $listenerId; - } - - protected function createAuthenticationSuccessHandler($container, $id, $config) + protected function createAuthenticationSuccessHandler(ContainerBuilder $container, string $id, array $config) { $successHandlerId = $this->getSuccessHandlerId($id); $options = array_intersect_key($config, $this->defaultSuccessHandlerOptions); @@ -178,13 +86,13 @@ protected function createAuthenticationSuccessHandler($container, $id, $config) } else { $successHandler = $container->setDefinition($successHandlerId, new ChildDefinition('security.authentication.success_handler')); $successHandler->addMethodCall('setOptions', [$options]); - $successHandler->addMethodCall('setProviderKey', [$id]); + $successHandler->addMethodCall('setFirewallName', [$id]); } return $successHandlerId; } - protected function createAuthenticationFailureHandler($container, $id, $config) + protected function createAuthenticationFailureHandler(ContainerBuilder $container, string $id, array $config) { $id = $this->getFailureHandlerId($id); $options = array_intersect_key($config, $this->defaultFailureHandlerOptions); @@ -201,12 +109,12 @@ protected function createAuthenticationFailureHandler($container, $id, $config) return $id; } - protected function getSuccessHandlerId($id) + protected function getSuccessHandlerId(string $id) { return 'security.authentication.success_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); } - protected function getFailureHandlerId($id) + protected function getFailureHandlerId(string $id) { return 'security.authentication.failure_handler.'.$id.'.'.str_replace('-', '_', $this->getKey()); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php deleted file mode 100644 index eb3c930afe379..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Parameter; - -/** - * @author Wouter de Jong - */ -class AnonymousFactory implements SecurityFactoryInterface -{ - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) - { - if (null === $config['secret']) { - $config['secret'] = new Parameter('container.build_hash'); - } - - $listenerId = 'security.authentication.listener.anonymous.'.$id; - $container - ->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.anonymous')) - ->replaceArgument(1, $config['secret']) - ; - - $providerId = 'security.authentication.provider.anonymous.'.$id; - $container - ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.anonymous')) - ->replaceArgument(0, $config['secret']) - ; - - return [$providerId, $listenerId, $defaultEntryPoint]; - } - - public function getPosition() - { - return 'anonymous'; - } - - public function getKey() - { - return 'anonymous'; - } - - public function addConfiguration(NodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifTrue(function ($v) { return 'lazy' === $v; }) - ->then(function ($v) { return ['lazy' => true]; }) - ->end() - ->children() - ->booleanNode('lazy')->defaultFalse()->end() - ->scalarNode('secret')->defaultNull()->end() - ->end() - ; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php new file mode 100644 index 0000000000000..1ef3f74f79aa5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Wouter de Jong + */ +interface AuthenticatorFactoryInterface +{ + /** + * Defines the priority at which the authenticator is called. + */ + public function getPriority(): int; + + /** + * Defines the configuration key used to reference the provider + * in the firewall configuration. + */ + public function getKey(): string; + + public function addConfiguration(NodeDefinition $builder); + + /** + * Creates the authenticator service(s) for the provided configuration. + * + * @return string|string[] The authenticator service ID(s) to be used by the firewall + */ + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string|array; +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php new file mode 100644 index 0000000000000..269b6e85a925d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Wouter de Jong + * + * @internal + */ +class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface +{ + public function getPriority(): int + { + return 0; + } + + public function getKey(): string + { + return 'custom_authenticators'; + } + + /** + * @param ArrayNodeDefinition $builder + */ + public function addConfiguration(NodeDefinition $builder) + { + $builder + ->info('An array of service ids for all of your "authenticators"') + ->requiresAtLeastOneElement() + ->prototype('scalar')->end(); + + // get the parent array node builder ("firewalls") from inside the children builder + $factoryRootNode = $builder->end()->end(); + $factoryRootNode + ->fixXmlConfig('custom_authenticator') + ->validate() + ->ifTrue(function ($v) { return isset($v['custom_authenticators']) && empty($v['custom_authenticators']); }) + ->then(function ($v) { + unset($v['custom_authenticators']); + + return $v; + }) + ->end() + ; + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): array + { + return $config; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.php new file mode 100644 index 0000000000000..c4842010a779f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FirewallListenerFactoryInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Can be implemented by a security factory to add a listener to the firewall. + * + * @author Christian Scheb + */ +interface FirewallListenerFactoryInterface +{ + /** + * Creates the firewall listener services for the provided configuration. + * + * @return string[] The listener service IDs to be used by the firewall + */ + public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array; +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 24f0b98ae6fbb..bc5ce52376c0f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -21,24 +22,30 @@ * * @author Fabien Potencier * @author Johannes M. Schmitt + * + * @internal */ class FormLoginFactory extends AbstractFactory { + public const PRIORITY = -30; + public function __construct() { $this->addOption('username_parameter', '_username'); $this->addOption('password_parameter', '_password'); $this->addOption('csrf_parameter', '_csrf_token'); $this->addOption('csrf_token_id', 'authenticate'); + $this->addOption('enable_csrf', false); $this->addOption('post_only', true); + $this->addOption('form_only', false); } - public function getPosition() + public function getPriority(): int { - return 'form'; + return self::PRIORITY; } - public function getKey() + public function getKey(): string { return 'form-login'; } @@ -54,46 +61,25 @@ public function addConfiguration(NodeDefinition $node) ; } - protected function getListenerId() - { - return 'security.authentication.listener.form'; - } - - protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) - { - $provider = 'security.authentication.provider.dao.'.$id; - $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.dao')) - ->replaceArgument(0, new Reference($userProviderId)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->replaceArgument(2, $id) - ; - - return $provider; - } - - protected function createListener($container, $id, $config, $userProvider) + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $listenerId = parent::createListener($container, $id, $config, $userProvider); - - $container - ->getDefinition($listenerId) - ->addArgument(isset($config['csrf_token_generator']) ? new Reference($config['csrf_token_generator']) : null) - ; - - return $listenerId; - } - - protected function createEntryPoint($container, $id, $config, $defaultEntryPoint) - { - $entryPointId = 'security.authentication.form_entry_point.'.$id; - $container - ->setDefinition($entryPointId, new ChildDefinition('security.authentication.form_entry_point')) - ->addArgument(new Reference('security.http_utils')) - ->addArgument($config['login_path']) - ->addArgument($config['use_forward']) - ; - - return $entryPointId; + if (isset($config['csrf_token_generator'])) { + throw new InvalidConfigurationException('The "csrf_token_generator" on "form_login" does not exist, use "enable_csrf" instead.'); + } + + $authenticatorId = 'security.authenticator.form_login.'.$firewallName; + $options = array_intersect_key($config, $this->options); + $authenticator = $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config))) + ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config))) + ->replaceArgument(4, $options); + + if ($options['use_forward'] ?? false) { + $authenticator->addMethodCall('setHttpKernel', [new Reference('http_kernel')]); + } + + return $authenticatorId; } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php index 59ba5e45f8f90..a439ca0adf316 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php @@ -12,41 +12,18 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; /** * FormLoginLdapFactory creates services for form login ldap authentication. * * @author Grégoire Pineau * @author Charles Sarrazin + * + * @internal */ class FormLoginLdapFactory extends FormLoginFactory { - protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) - { - $provider = 'security.authentication.provider.ldap_bind.'.$id; - $definition = $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.ldap_bind')) - ->replaceArgument(0, new Reference($userProviderId)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->replaceArgument(2, $id) - ->replaceArgument(3, new Reference($config['service'])) - ->replaceArgument(4, $config['dn_string']) - ->replaceArgument(6, $config['search_dn']) - ->replaceArgument(7, $config['search_password']) - ; - - if (!empty($config['query_string'])) { - if ('' === $config['search_dn'] || '' === $config['search_password']) { - @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw an exception in Symfony 5.0.', \E_USER_DEPRECATED); - } - $definition->addMethodCall('setQueryString', [$config['query_string']]); - } - - return $provider; - } + use LdapFactoryTrait; public function addConfiguration(NodeDefinition $node) { @@ -62,9 +39,4 @@ public function addConfiguration(NodeDefinition $node) ->end() ; } - - public function getKey() - { - return 'form-login-ldap'; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php deleted file mode 100644 index 45a457aef8a62..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * Configures the "guard" authentication provider key under a firewall. - * - * @author Ryan Weaver - */ -class GuardAuthenticationFactory implements SecurityFactoryInterface -{ - public function getPosition() - { - return 'pre_auth'; - } - - public function getKey() - { - return 'guard'; - } - - public function addConfiguration(NodeDefinition $node) - { - $node - ->fixXmlConfig('authenticator') - ->children() - ->scalarNode('provider') - ->info('A key from the "providers" section of your security config, in case your user provider is different than the firewall') - ->end() - ->scalarNode('entry_point') - ->info('A service id (of one of your authenticators) whose start() method should be called when an anonymous user hits a page that requires authentication') - ->defaultValue(null) - ->end() - ->arrayNode('authenticators') - ->info('An array of service ids for all of your "authenticators"') - ->requiresAtLeastOneElement() - ->prototype('scalar')->end() - ->end() - ->end() - ; - } - - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) - { - $authenticatorIds = $config['authenticators']; - $authenticatorReferences = []; - foreach ($authenticatorIds as $authenticatorId) { - $authenticatorReferences[] = new Reference($authenticatorId); - } - - $authenticators = new IteratorArgument($authenticatorReferences); - - // configure the GuardAuthenticationFactory to have the dynamic constructor arguments - $providerId = 'security.authentication.provider.guard.'.$id; - $container - ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.guard')) - ->replaceArgument(0, $authenticators) - ->replaceArgument(1, new Reference($userProvider)) - ->replaceArgument(2, $id) - ->replaceArgument(3, new Reference('security.user_checker.'.$id)) - ; - - // listener - $listenerId = 'security.authentication.listener.guard.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.guard')); - $listener->replaceArgument(2, $id); - $listener->replaceArgument(3, $authenticators); - - // determine the entryPointId to use - $entryPointId = $this->determineEntryPoint($defaultEntryPoint, $config); - - // this is always injected - then the listener decides if it should be used - $container - ->getDefinition($listenerId) - ->addTag('security.remember_me_aware', ['id' => $id, 'provider' => $userProvider]); - - return [$providerId, $listenerId, $entryPointId]; - } - - private function determineEntryPoint(?string $defaultEntryPointId, array $config): string - { - if ($defaultEntryPointId) { - // explode if they've configured the entry_point, but there is already one - if ($config['entry_point']) { - throw new \LogicException(sprintf('The guard authentication provider cannot use the "%s" entry_point because another entry point is already configured by another provider! Either remove the other provider or move the entry_point configuration as a root key under your firewall (i.e. at the same level as "guard").', $config['entry_point'])); - } - - return $defaultEntryPointId; - } - - if ($config['entry_point']) { - // if it's configured explicitly, use it! - return $config['entry_point']; - } - - $authenticatorIds = $config['authenticators']; - if (1 == \count($authenticatorIds)) { - // if there is only one authenticator, use that as the entry point - return array_shift($authenticatorIds); - } - - // we have multiple entry points - we must ask them to configure one - throw new \LogicException(sprintf('Because you have multiple guard authenticators, you need to set the "guard.entry_point" key to one of your authenticators (%s).', implode(', ', $authenticatorIds))); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index f3b5bc167e64e..02f2009ac1eca 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -20,38 +20,30 @@ * HttpBasicFactory creates services for HTTP basic authentication. * * @author Fabien Potencier + * + * @internal */ -class HttpBasicFactory implements SecurityFactoryInterface +class HttpBasicFactory implements AuthenticatorFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public const PRIORITY = -50; + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $provider = 'security.authentication.provider.dao.'.$id; + $authenticatorId = 'security.authenticator.http_basic.'.$firewallName; $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.dao')) - ->replaceArgument(0, new Reference($userProvider)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->replaceArgument(2, $id) - ; + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.http_basic')) + ->replaceArgument(0, $config['realm']) + ->replaceArgument(1, new Reference($userProviderId)); - // entry point - $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint); - - // listener - $listenerId = 'security.authentication.listener.basic.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.basic')); - $listener->replaceArgument(2, $id); - $listener->replaceArgument(3, new Reference($entryPointId)); - $listener->addMethodCall('setSessionAuthenticationStrategy', [new Reference('security.authentication.session_strategy.'.$id)]); - - return [$provider, $listenerId, $entryPointId]; + return $authenticatorId; } - public function getPosition() + public function getPriority(): int { - return 'http'; + return self::PRIORITY; } - public function getKey() + public function getKey(): string { return 'http-basic'; } @@ -65,19 +57,4 @@ public function addConfiguration(NodeDefinition $node) ->end() ; } - - protected function createEntryPoint($container, $id, $config, $defaultEntryPoint) - { - if (null !== $defaultEntryPoint) { - return $defaultEntryPoint; - } - - $entryPointId = 'security.authentication.basic_entry_point.'.$id; - $container - ->setDefinition($entryPointId, new ChildDefinition('security.authentication.basic_entry_point')) - ->addArgument($config['realm']) - ; - - return $entryPointId; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php index 8980c88140834..0c63b21c63aaa 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php @@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Core\Exception\LogicException; /** * HttpBasicFactory creates services for HTTP basic authentication. @@ -22,10 +23,14 @@ * @author Fabien Potencier * @author Grégoire Pineau * @author Charles Sarrazin + * + * @internal */ class HttpBasicLdapFactory extends HttpBasicFactory { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + use LdapFactoryTrait; + + public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint): array { $provider = 'security.authentication.provider.ldap_bind.'.$id; $definition = $container @@ -40,11 +45,18 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, ; // entry point - $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint); + $entryPointId = $defaultEntryPoint; + + if (null === $entryPointId) { + $entryPointId = 'security.authentication.basic_entry_point.'.$id; + $container + ->setDefinition($entryPointId, new ChildDefinition('security.authentication.basic_entry_point')) + ->addArgument($config['realm']); + } if (!empty($config['query_string'])) { if ('' === $config['search_dn'] || '' === $config['search_password']) { - @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw an exception in Symfony 5.0.', \E_USER_DEPRECATED); + throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.'); } $definition->addMethodCall('setQueryString', [$config['query_string']]); } @@ -72,9 +84,4 @@ public function addConfiguration(NodeDefinition $node) ->end() ; } - - public function getKey() - { - return 'http-basic-ldap'; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php index a660401dbea34..9307cba86e3f3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginFactory.php @@ -19,9 +19,13 @@ * JsonLoginFactory creates services for JSON login authentication. * * @author Kévin Dunglas + * + * @internal */ class JsonLoginFactory extends AbstractFactory { + public const PRIORITY = -40; + public function __construct() { $this->addOption('username_path', 'username'); @@ -30,70 +34,30 @@ public function __construct() $this->defaultSuccessHandlerOptions = []; } - /** - * {@inheritdoc} - */ - public function getPosition() + public function getPriority(): int { - return 'form'; + return self::PRIORITY; } /** * {@inheritdoc} */ - public function getKey() + public function getKey(): string { return 'json-login'; } - /** - * {@inheritdoc} - */ - protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $provider = 'security.authentication.provider.dao.'.$id; + $authenticatorId = 'security.authenticator.json_login.'.$firewallName; + $options = array_intersect_key($config, $this->options); $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.dao')) - ->replaceArgument(0, new Reference($userProviderId)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->replaceArgument(2, $id) - ; - - return $provider; - } - - /** - * {@inheritdoc} - */ - protected function getListenerId() - { - return 'security.authentication.listener.json'; - } - - /** - * {@inheritdoc} - */ - protected function isRememberMeAware($config) - { - return false; - } - - /** - * {@inheritdoc} - */ - protected function createListener($container, $id, $config, $userProvider) - { - $listenerId = $this->getListenerId(); - $listener = new ChildDefinition($listenerId); - $listener->replaceArgument(3, $id); - $listener->replaceArgument(4, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $id, $config)) : null); - $listener->replaceArgument(5, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $id, $config)) : null); - $listener->replaceArgument(6, array_intersect_key($config, $this->options)); - $listener->addMethodCall('setSessionAuthenticationStrategy', [new Reference('security.authentication.session_strategy.'.$id)]); - - $listenerId .= '.'.$id; - $container->setDefinition($listenerId, $listener); + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.json_login')) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)) : null) + ->replaceArgument(3, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null) + ->replaceArgument(4, $options); - return $listenerId; + return $authenticatorId; } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php index 5b1be3dbf9b6a..3b4ff7a048df2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php @@ -12,43 +12,15 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; /** * JsonLoginLdapFactory creates services for json login ldap authentication. + * + * @internal */ class JsonLoginLdapFactory extends JsonLoginFactory { - public function getKey() - { - return 'json-login-ldap'; - } - - protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) - { - $provider = 'security.authentication.provider.ldap_bind.'.$id; - $definition = $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.ldap_bind')) - ->replaceArgument(0, new Reference($userProviderId)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->replaceArgument(2, $id) - ->replaceArgument(3, new Reference($config['service'])) - ->replaceArgument(4, $config['dn_string']) - ->replaceArgument(6, $config['search_dn']) - ->replaceArgument(7, $config['search_password']) - ; - - if (!empty($config['query_string'])) { - if ('' === $config['search_dn'] || '' === $config['search_password']) { - @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw an exception in Symfony 5.0.', \E_USER_DEPRECATED); - } - $definition->addMethodCall('setQueryString', [$config['query_string']]); - } - - return $provider; - } + use LdapFactoryTrait; public function addConfiguration(NodeDefinition $node) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php new file mode 100644 index 0000000000000..8af8e4424b270 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LdapFactoryTrait.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener; +use Symfony\Component\Ldap\Security\LdapAuthenticator; + +/** + * A trait decorating the authenticator with LDAP functionality. + * + * @author Wouter de Jong + * + * @internal + */ +trait LdapFactoryTrait +{ + public function getKey(): string + { + return parent::getKey().'-ldap'; + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string + { + $key = str_replace('-', '_', $this->getKey()); + if (!class_exists(LdapAuthenticator::class)) { + throw new \LogicException(sprintf('The "%s" authenticator requires the "symfony/ldap" package version "5.1" or higher.', $key)); + } + + $authenticatorId = parent::createAuthenticator($container, $firewallName, $config, $userProviderId); + + $container->setDefinition('security.listener.'.$key.'.'.$firewallName, new Definition(CheckLdapCredentialsListener::class)) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName]) + ->addArgument(new Reference('security.ldap_locator')) + ; + + $ldapAuthenticatorId = 'security.authenticator.'.$key.'.'.$firewallName; + $definition = $container->setDefinition($ldapAuthenticatorId, new Definition(LdapAuthenticator::class)) + ->setArguments([ + new Reference($authenticatorId), + $config['service'], + $config['dn_string'], + $config['search_dn'], + $config['search_password'], + ]); + + if (!empty($config['query_string'])) { + if ('' === $config['search_dn'] || '' === $config['search_password']) { + throw new InvalidConfigurationException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.'); + } + + $definition->addArgument($config['query_string']); + } + + return $ldapAuthenticatorId; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php new file mode 100644 index 0000000000000..88e34192abf14 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\NodeBuilder; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler; + +/** + * @internal + */ +class LoginLinkFactory extends AbstractFactory +{ + public const PRIORITY = -20; + + public function addConfiguration(NodeDefinition $node) + { + /** @var NodeBuilder $builder */ + $builder = $node->fixXmlConfig('signature_property', 'signature_properties')->children(); + + $builder + ->scalarNode('check_route') + ->isRequired() + ->info('Route that will validate the login link - e.g. "app_login_link_verify".') + ->end() + ->scalarNode('check_post_only') + ->defaultFalse() + ->info('If true, only HTTP POST requests to "check_route" will be handled by the authenticator.') + ->end() + ->arrayNode('signature_properties') + ->isRequired() + ->prototype('scalar')->end() + ->requiresAtLeastOneElement() + ->info('An array of properties on your User that are used to sign the link. If any of these change, all existing links will become invalid.') + ->example(['email', 'password']) + ->end() + ->integerNode('lifetime') + ->defaultValue(600) + ->info('The lifetime of the login link in seconds.') + ->end() + ->integerNode('max_uses') + ->defaultNull() + ->info('Max number of times a login link can be used - null means unlimited within lifetime.') + ->end() + ->scalarNode('used_link_cache') + ->info('Cache service id used to expired links of max_uses is set.') + ->end() + ->scalarNode('success_handler') + ->info(sprintf('A service id that implements %s.', AuthenticationSuccessHandlerInterface::class)) + ->end() + ->scalarNode('failure_handler') + ->info(sprintf('A service id that implements %s.', AuthenticationFailureHandlerInterface::class)) + ->end() + ->scalarNode('provider') + ->info('The user provider to load users from.') + ->end() + ; + + foreach (array_merge($this->defaultSuccessHandlerOptions, $this->defaultFailureHandlerOptions) as $name => $default) { + if (\is_bool($default)) { + $builder->booleanNode($name)->defaultValue($default); + } else { + $builder->scalarNode($name)->defaultValue($default); + } + } + } + + public function getKey(): string + { + return 'login-link'; + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string + { + if (!class_exists(LoginLinkHandler::class)) { + throw new \LogicException('Login login link requires symfony/security-http:^5.2.'); + } + + if (!$container->hasDefinition('security.authenticator.login_link')) { + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../../Resources/config')); + $loader->load('security_authenticator_login_link.php'); + } + + if (null !== $config['max_uses'] && !isset($config['used_link_cache'])) { + $config['used_link_cache'] = 'security.authenticator.cache.expired_links'; + $defaultCacheDefinition = $container->getDefinition($config['used_link_cache']); + if (!$defaultCacheDefinition->hasTag('cache.pool')) { + $defaultCacheDefinition->addTag('cache.pool'); + } + } + + $expiredStorageId = null; + if (isset($config['used_link_cache'])) { + $expiredStorageId = 'security.authenticator.expired_login_link_storage.'.$firewallName; + $container + ->setDefinition($expiredStorageId, new ChildDefinition('security.authenticator.expired_login_link_storage')) + ->replaceArgument(0, new Reference($config['used_link_cache'])) + ->replaceArgument(1, $config['lifetime']); + } + + $signatureHasherId = 'security.authenticator.login_link_signature_hasher.'.$firewallName; + $container + ->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.abstract_login_link_signature_hasher')) + ->replaceArgument(1, $config['signature_properties']) + ->replaceArgument(3, $expiredStorageId ? new Reference($expiredStorageId) : null) + ->replaceArgument(4, $config['max_uses'] ?? null) + ; + + $linkerId = 'security.authenticator.login_link_handler.'.$firewallName; + $linkerOptions = [ + 'route_name' => $config['check_route'], + 'lifetime' => $config['lifetime'], + ]; + $container + ->setDefinition($linkerId, new ChildDefinition('security.authenticator.abstract_login_link_handler')) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(2, new Reference($signatureHasherId)) + ->replaceArgument(3, $linkerOptions) + ->addTag('security.authenticator.login_linker', ['firewall' => $firewallName]) + ; + + $authenticatorId = 'security.authenticator.login_link.'.$firewallName; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.login_link')) + ->replaceArgument(0, new Reference($linkerId)) + ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config))) + ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config))) + ->replaceArgument(4, [ + 'check_route' => $config['check_route'], + 'check_post_only' => $config['check_post_only'], + ]); + + return $authenticatorId; + } + + public function getPriority(): int + { + return self::PRIORITY; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php new file mode 100644 index 0000000000000..5b5c1a55bce0a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface; +use Symfony\Component\RateLimiter\RateLimiterFactory; +use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener; +use Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter; + +/** + * @author Wouter de Jong + * + * @internal + */ +class LoginThrottlingFactory implements AuthenticatorFactoryInterface +{ + public function getPriority(): int + { + // this factory doesn't register any authenticators, this priority doesn't matter + return 0; + } + + public function getKey(): string + { + return 'login_throttling'; + } + + /** + * @param ArrayNodeDefinition $builder + */ + public function addConfiguration(NodeDefinition $builder) + { + $builder + ->children() + ->scalarNode('limiter')->info(sprintf('A service id implementing "%s".', RequestRateLimiterInterface::class))->end() + ->integerNode('max_attempts')->defaultValue(5)->end() + ->scalarNode('interval')->defaultValue('1 minute')->end() + ->scalarNode('lock_factory')->info('The service ID of the lock factory used by the login rate limiter (or null to disable locking)')->defaultNull()->end() + ->end(); + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): array + { + if (!class_exists(LoginThrottlingListener::class)) { + throw new \LogicException('Login throttling requires symfony/security-http:^5.2.'); + } + + if (!class_exists(RateLimiterFactory::class)) { + throw new \LogicException('Login throttling requires the Rate Limiter component. Try running "composer require symfony/rate-limiter".'); + } + + if (!isset($config['limiter'])) { + if (!class_exists(FrameworkExtension::class) || !method_exists(FrameworkExtension::class, 'registerRateLimiter')) { + throw new \LogicException('You must either configure a rate limiter for "security.firewalls.'.$firewallName.'.login_throttling" or install symfony/framework-bundle:^5.2.'); + } + + $limiterOptions = [ + 'policy' => 'fixed_window', + 'limit' => $config['max_attempts'], + 'interval' => $config['interval'], + 'lock_factory' => $config['lock_factory'], + ]; + FrameworkExtension::registerRateLimiter($container, $localId = '_login_local_'.$firewallName, $limiterOptions); + + $limiterOptions['limit'] = 5 * $config['max_attempts']; + FrameworkExtension::registerRateLimiter($container, $globalId = '_login_global_'.$firewallName, $limiterOptions); + + $container->register($config['limiter'] = 'security.login_throttling.'.$firewallName.'.limiter', DefaultLoginRateLimiter::class) + ->addArgument(new Reference('limiter.'.$globalId)) + ->addArgument(new Reference('limiter.'.$localId)) + ; + } + + $container + ->setDefinition('security.listener.login_throttling.'.$firewallName, new ChildDefinition('security.listener.login_throttling')) + ->replaceArgument(1, new Reference($config['limiter'])) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName]); + + return []; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 153028165c987..b2cd3fed196ba 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -11,14 +11,28 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; +use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider; +use Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler; use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\Security\Core\Authentication\RememberMe\CacheTokenVerifier; -class RememberMeFactory implements SecurityFactoryInterface +/** + * @internal + */ +class RememberMeFactory implements AuthenticatorFactoryInterface, PrependExtensionInterface { + public const PRIORITY = -50; + protected $options = [ 'name' => 'REMEMBERME', 'lifetime' => 31536000, @@ -31,101 +45,89 @@ class RememberMeFactory implements SecurityFactoryInterface 'remember_me_parameter' => '_remember_me', ]; - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - // authentication provider - $authProviderId = 'security.authentication.provider.rememberme.'.$id; - $container - ->setDefinition($authProviderId, new ChildDefinition('security.authentication.provider.rememberme')) - ->replaceArgument(0, new Reference('security.user_checker.'.$id)) - ->addArgument($config['secret']) - ->addArgument($id) - ; + if (!$container->hasDefinition('security.authenticator.remember_me')) { + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../../Resources/config')); + $loader->load('security_authenticator_remember_me.php'); + } - // remember me services - if (isset($config['token_provider'])) { - $templateId = 'security.authentication.rememberme.services.persistent'; - $rememberMeServicesId = $templateId.'.'.$id; - } else { - $templateId = 'security.authentication.rememberme.services.simplehash'; - $rememberMeServicesId = $templateId.'.'.$id; + if ('auto' === $config['secure']) { + $config['secure'] = null; } - if ($container->hasDefinition('security.logout_listener.'.$id)) { - $container - ->getDefinition('security.logout_listener.'.$id) - ->addMethodCall('addHandler', [new Reference($rememberMeServicesId)]) - ; + // create remember me handler (which manage the remember-me cookies) + $rememberMeHandlerId = 'security.authenticator.remember_me_handler.'.$firewallName; + if (isset($config['service']) && isset($config['token_provider'])) { + throw new InvalidConfigurationException(sprintf('You cannot use both "service" and "token_provider" in "security.firewalls.%s.remember_me".', $firewallName)); } - $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); - $rememberMeServices->replaceArgument(1, $config['secret']); - $rememberMeServices->replaceArgument(2, $id); + if (isset($config['service'])) { + $container->register($rememberMeHandlerId, DecoratedRememberMeHandler::class) + ->addArgument(new Reference($config['service'])) + ->addTag('security.remember_me_handler', ['firewall' => $firewallName]); + } elseif (isset($config['token_provider'])) { + $tokenProviderId = $this->createTokenProvider($container, $firewallName, $config['token_provider']); + $tokenVerifier = $this->createTokenVerifier($container, $firewallName, $config['token_verifier'] ?? null); + $container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.persistent_remember_me_handler')) + ->replaceArgument(0, new Reference($tokenProviderId)) + ->replaceArgument(2, new Reference($userProviderId)) + ->replaceArgument(4, $config) + ->replaceArgument(6, $tokenVerifier) + ->addTag('security.remember_me_handler', ['firewall' => $firewallName]); + } else { + $signatureHasherId = 'security.authenticator.remember_me_signature_hasher.'.$firewallName; + $container->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.remember_me_signature_hasher')) + ->replaceArgument(1, $config['signature_properties']) + ; - if (isset($config['token_provider'])) { - $rememberMeServices->addMethodCall('setTokenProvider', [ - new Reference($config['token_provider']), - ]); + $container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.signature_remember_me_handler')) + ->replaceArgument(0, new Reference($signatureHasherId)) + ->replaceArgument(1, new Reference($userProviderId)) + ->replaceArgument(3, $config) + ->addTag('security.remember_me_handler', ['firewall' => $firewallName]); } - // remember-me options - $mergedOptions = array_intersect_key($config, $this->options); - if ('auto' === $mergedOptions['secure']) { - $mergedOptions['secure'] = null; - } + // create check remember me conditions listener (which checks if a remember-me cookie is supported and requested) + $rememberMeConditionsListenerId = 'security.listener.check_remember_me_conditions.'.$firewallName; + $container->setDefinition($rememberMeConditionsListenerId, new ChildDefinition('security.listener.check_remember_me_conditions')) + ->replaceArgument(0, array_intersect_key($config, ['always_remember_me' => true, 'remember_me_parameter' => true])) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName]) + ; - $rememberMeServices->replaceArgument(3, $mergedOptions); + // create remember me listener (which executes the remember me services for other authenticators and logout) + $rememberMeListenerId = 'security.listener.remember_me.'.$firewallName; + $container->setDefinition($rememberMeListenerId, new ChildDefinition('security.listener.remember_me')) + ->replaceArgument(0, new Reference($rememberMeHandlerId)) + ->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName]) + ; + + // create remember me authenticator (which re-authenticates the user based on the remember-me cookie) + $authenticatorId = 'security.authenticator.remember_me.'.$firewallName; + $container + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me')) + ->replaceArgument(0, new Reference($rememberMeHandlerId)) + ->replaceArgument(3, $config['name'] ?? $this->options['name']) + ; - // attach to remember-me aware listeners - $userProviders = []; foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) { - foreach ($attributes as $attribute) { - if (!isset($attribute['id']) || $attribute['id'] !== $id) { - continue; - } - - if (!isset($attribute['provider'])) { - throw new \RuntimeException('Each "security.remember_me_aware" tag must have a provider attribute.'); - } - - // context listeners don't need a provider - if ('none' !== $attribute['provider']) { - $userProviders[] = new Reference($attribute['provider']); - } - - $container - ->getDefinition($serviceId) - ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)]) - ; + // register ContextListener + if ('security.context_listener' === substr($serviceId, 0, 25)) { + continue; } - } - if ($config['user_providers']) { - $userProviders = []; - foreach ($config['user_providers'] as $providerName) { - $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName); - } - } - if (0 === \count($userProviders)) { - throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.'); - } - $rememberMeServices->replaceArgument(0, array_unique($userProviders)); - - // remember-me listener - $listenerId = 'security.authentication.listener.rememberme.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.rememberme')); - $listener->replaceArgument(1, new Reference($rememberMeServicesId)); - $listener->replaceArgument(5, $config['catch_exceptions']); + throw new \LogicException(sprintf('Symfony Authenticator Security dropped support for the "security.remember_me_aware" tag, service "%s" will no longer work as expected.', $serviceId)); + } - return [$authProviderId, $listenerId, $defaultEntryPoint]; + return $authenticatorId; } - public function getPosition() + public function getPriority(): int { - return 'remember_me'; + return self::PRIORITY; } - public function getKey() + public function getKey(): string { return 'remember-me'; } @@ -139,7 +141,7 @@ public function addConfiguration(NodeDefinition $node) $builder ->scalarNode('secret')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('token_provider')->end() + ->scalarNode('service')->end() ->arrayNode('user_providers') ->beforeNormalization() ->ifString()->then(function ($v) { return [$v]; }) @@ -147,7 +149,30 @@ public function addConfiguration(NodeDefinition $node) ->prototype('scalar')->end() ->end() ->booleanNode('catch_exceptions')->defaultTrue()->end() - ; + ->arrayNode('signature_properties') + ->prototype('scalar')->end() + ->requiresAtLeastOneElement() + ->info('An array of properties on your User that are used to sign the remember-me cookie. If any of these change, all existing cookies will become invalid.') + ->example(['email', 'password']) + ->defaultValue(['password']) + ->end() + ->arrayNode('token_provider') + ->beforeNormalization() + ->ifString()->then(function ($v) { return ['service' => $v]; }) + ->end() + ->children() + ->scalarNode('service')->info('The service ID of a custom rememberme token provider.')->end() + ->arrayNode('doctrine') + ->canBeEnabled() + ->children() + ->scalarNode('connection')->defaultNull()->end() + ->end() + ->end() + ->end() + ->end() + ->scalarNode('token_verifier') + ->info('The service ID of a custom rememberme token verifier.') + ->end(); foreach ($this->options as $name => $value) { if ('secure' === $name) { @@ -163,4 +188,117 @@ public function addConfiguration(NodeDefinition $node) } } } + + private function generateRememberMeServicesTemplateId(array $config, string $id): string + { + if (isset($config['service'])) { + return $config['service']; + } + + if (isset($config['token_provider'])) { + return 'security.authentication.rememberme.services.persistent'; + } + + return 'security.authentication.rememberme.services.simplehash'; + } + + private function createRememberMeServices(ContainerBuilder $container, string $id, string $templateId, array $userProviders, array $config): void + { + $rememberMeServicesId = $templateId.'.'.$id; + + $rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId)); + $rememberMeServices->replaceArgument(1, $config['secret']); + $rememberMeServices->replaceArgument(2, $id); + + if (isset($config['token_provider'])) { + $tokenProviderId = $this->createTokenProvider($container, $id, $config['token_provider']); + $rememberMeServices->addMethodCall('setTokenProvider', [new Reference($tokenProviderId)]); + } + + // remember-me options + $mergedOptions = array_intersect_key($config, $this->options); + if ('auto' === $mergedOptions['secure']) { + $mergedOptions['secure'] = null; + } + + $rememberMeServices->replaceArgument(3, $mergedOptions); + + if ($config['user_providers']) { + $userProviders = []; + foreach ($config['user_providers'] as $providerName) { + $userProviders[] = new Reference('security.user.provider.concrete.'.$providerName); + } + } + + if (0 === \count($userProviders)) { + throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.'); + } + + $rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders))); + } + + private function createTokenProvider(ContainerBuilder $container, string $firewallName, array $config): string + { + $tokenProviderId = $config['service'] ?? false; + if ($config['doctrine']['enabled'] ?? false) { + if (!class_exists(DoctrineTokenProvider::class)) { + throw new InvalidConfigurationException('Cannot use the "doctrine" token provider for "remember_me" because the Doctrine Bridge is not installed. Try running "composer require symfony/doctrine-bridge".'); + } + + if (null === $config['doctrine']['connection']) { + $connectionId = 'database_connection'; + } else { + $connectionId = 'doctrine.dbal.'.$config['doctrine']['connection'].'_connection'; + } + + $tokenProviderId = 'security.remember_me.doctrine_token_provider.'.$firewallName; + $container->register($tokenProviderId, DoctrineTokenProvider::class) + ->addArgument(new Reference($connectionId)); + } + + if (!$tokenProviderId) { + throw new InvalidConfigurationException(sprintf('No token provider was set for firewall "%s". Either configure a service ID or set "remember_me.token_provider.doctrine" to true.', $firewallName)); + } + + return $tokenProviderId; + } + + private function createTokenVerifier(ContainerBuilder $container, string $firewallName, ?string $serviceId): Reference + { + if ($serviceId) { + return new Reference($serviceId); + } + + $tokenVerifierId = 'security.remember_me.token_verifier.'.$firewallName; + + $container->register($tokenVerifierId, CacheTokenVerifier::class) + ->addArgument(new Reference('cache.security_token_verifier', ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->addArgument(60) + ->addArgument('rememberme-'.$firewallName.'-stale-'); + + return new Reference($tokenVerifierId, ContainerInterface::NULL_ON_INVALID_REFERENCE); + } + + /** + * {@inheritdoc} + */ + public function prepend(ContainerBuilder $container) + { + $rememberMeSecureDefault = false; + $rememberMeSameSiteDefault = null; + + if (!isset($container->getExtensions()['framework'])) { + return; + } + + foreach ($container->getExtensionConfig('framework') as $config) { + if (isset($config['session']) && \is_array($config['session'])) { + $rememberMeSecureDefault = $config['session']['cookie_secure'] ?? $rememberMeSecureDefault; + $rememberMeSameSiteDefault = \array_key_exists('cookie_samesite', $config['session']) ? $config['session']['cookie_samesite'] : $rememberMeSameSiteDefault; + } + } + + $this->options['secure'] = $rememberMeSecureDefault; + $this->options['samesite'] = $rememberMeSameSiteDefault; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php index 176f65b13578a..de79af1494f42 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php @@ -21,34 +21,32 @@ * * @author Fabien Potencier * @author Maxime Douailin + * + * @internal */ -class RemoteUserFactory implements SecurityFactoryInterface +class RemoteUserFactory implements AuthenticatorFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public const PRIORITY = -10; + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $providerId = 'security.authentication.provider.pre_authenticated.'.$id; + $authenticatorId = 'security.authenticator.remote_user.'.$firewallName; $container - ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.pre_authenticated')) - ->replaceArgument(0, new Reference($userProvider)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->addArgument($id) + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remote_user')) + ->replaceArgument(0, new Reference($userProviderId)) + ->replaceArgument(2, $firewallName) + ->replaceArgument(3, $config['user']) ; - $listenerId = 'security.authentication.listener.remote_user.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.remote_user')); - $listener->replaceArgument(2, $id); - $listener->replaceArgument(3, $config['user']); - $listener->addMethodCall('setSessionAuthenticationStrategy', [new Reference('security.authentication.session_strategy.'.$id)]); - - return [$providerId, $listenerId, $defaultEntryPoint]; + return $authenticatorId; } - public function getPosition() + public function getPriority(): int { - return 'pre_auth'; + return self::PRIORITY; } - public function getKey() + public function getKey(): string { return 'remote-user'; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php deleted file mode 100644 index 06bae0c7b91ee..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * SecurityFactoryInterface is the interface for all security authentication listener. - * - * @author Fabien Potencier - */ -interface SecurityFactoryInterface -{ - /** - * Configures the container services required to use the authentication listener. - * - * @param string $id The unique id of the firewall - * @param array $config The options array for the listener - * @param string $userProviderId The service id of the user provider - * @param string|null $defaultEntryPointId - * - * @return array containing three values: - * - the provider id - * - the listener id - * - the entry point id - */ - public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId); - - /** - * Defines the position at which the provider is called. - * Possible values: pre_auth, form, http, and remember_me. - * - * @return string - */ - public function getPosition(); - - /** - * Defines the configuration key used to reference the provider - * in the firewall configuration. - * - * @return string - */ - public function getKey(); - - public function addConfiguration(NodeDefinition $builder); -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php deleted file mode 100644 index 4fa128273db5b..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php +++ /dev/null @@ -1,87 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @author Jordi Boggiano - * - * @deprecated since Symfony 4.2, use Guard instead. - */ -class SimpleFormFactory extends FormLoginFactory -{ - public function __construct(bool $triggerDeprecation = true) - { - parent::__construct(); - - $this->addOption('authenticator', null); - - if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use Guard instead.', __CLASS__), \E_USER_DEPRECATED); - } - } - - public function getKey() - { - return 'simple-form'; - } - - public function addConfiguration(NodeDefinition $node) - { - parent::addConfiguration($node); - - $node->children() - ->scalarNode('authenticator')->cannotBeEmpty()->end() - ->end(); - } - - protected function getListenerId() - { - return 'security.authentication.listener.simple_form'; - } - - protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) - { - $provider = 'security.authentication.provider.simple_form.'.$id; - $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.simple')) - ->replaceArgument(0, new Reference($config['authenticator'])) - ->replaceArgument(1, new Reference($userProviderId)) - ->replaceArgument(2, $id) - ->replaceArgument(3, new Reference('security.user_checker.'.$id)) - ; - - return $provider; - } - - protected function createListener($container, $id, $config, $userProvider) - { - $listenerId = parent::createListener($container, $id, $config, $userProvider); - - $simpleAuthHandlerId = 'security.authentication.simple_success_failure_handler.'.$id; - $simpleAuthHandler = $container->setDefinition($simpleAuthHandlerId, new ChildDefinition('security.authentication.simple_success_failure_handler')); - $simpleAuthHandler->replaceArgument(0, new Reference($config['authenticator'])); - $simpleAuthHandler->replaceArgument(1, new Reference($this->getSuccessHandlerId($id))); - $simpleAuthHandler->replaceArgument(2, new Reference($this->getFailureHandlerId($id))); - - $listener = $container->getDefinition($listenerId); - $listener->replaceArgument(5, new Reference($simpleAuthHandlerId)); - $listener->replaceArgument(6, new Reference($simpleAuthHandlerId)); - $listener->addArgument(new Reference($config['authenticator'])); - - return $listenerId; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php deleted file mode 100644 index 01a8521c34b17..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Symfony\Component\Config\Definition\Builder\NodeDefinition; -use Symfony\Component\DependencyInjection\ChildDefinition; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -/** - * @author Jordi Boggiano - * - * @deprecated since Symfony 4.2, use Guard instead. - */ -class SimplePreAuthenticationFactory implements SecurityFactoryInterface -{ - public function __construct(bool $triggerDeprecation = true) - { - if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use Guard instead.', __CLASS__), \E_USER_DEPRECATED); - } - } - - public function getPosition() - { - return 'pre_auth'; - } - - public function getKey() - { - return 'simple-preauth'; - } - - public function addConfiguration(NodeDefinition $node) - { - $node - ->children() - ->scalarNode('provider')->end() - ->scalarNode('authenticator')->cannotBeEmpty()->end() - ->end() - ; - } - - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) - { - $provider = 'security.authentication.provider.simple_preauth.'.$id; - $container - ->setDefinition($provider, new ChildDefinition('security.authentication.provider.simple')) - ->replaceArgument(0, new Reference($config['authenticator'])) - ->replaceArgument(1, new Reference($userProvider)) - ->replaceArgument(2, $id) - ->replaceArgument(3, new Reference('security.user_checker.'.$id)) - ; - - // listener - $listenerId = 'security.authentication.listener.simple_preauth.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.simple_preauth')); - $listener->replaceArgument(2, $id); - $listener->replaceArgument(3, new Reference($config['authenticator'])); - $listener->addMethodCall('setSessionAuthenticationStrategy', [new Reference('security.authentication.session_strategy.'.$id)]); - - return [$provider, $listenerId, null]; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index 35879ee4956b9..f59783defd11c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -20,36 +20,33 @@ * X509Factory creates services for X509 certificate authentication. * * @author Fabien Potencier + * + * @internal */ -class X509Factory implements SecurityFactoryInterface +class X509Factory implements AuthenticatorFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public const PRIORITY = -10; + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string { - $providerId = 'security.authentication.provider.pre_authenticated.'.$id; + $authenticatorId = 'security.authenticator.x509.'.$firewallName; $container - ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.pre_authenticated')) - ->replaceArgument(0, new Reference($userProvider)) - ->replaceArgument(1, new Reference('security.user_checker.'.$id)) - ->addArgument($id) + ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.x509')) + ->replaceArgument(0, new Reference($userProviderId)) + ->replaceArgument(2, $firewallName) + ->replaceArgument(3, $config['user']) + ->replaceArgument(4, $config['credentials']) ; - // listener - $listenerId = 'security.authentication.listener.x509.'.$id; - $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.x509')); - $listener->replaceArgument(2, $id); - $listener->replaceArgument(3, $config['user']); - $listener->replaceArgument(4, $config['credentials']); - $listener->addMethodCall('setSessionAuthenticationStrategy', [new Reference('security.authentication.session_strategy.'.$id)]); - - return [$providerId, $listenerId, $defaultEntryPoint]; + return $authenticatorId; } - public function getPosition() + public function getPriority(): int { - return 'pre_auth'; + return self::PRIORITY; } - public function getKey() + public function getKey(): string { return 'x509'; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php index 1e76601105022..0abb1ce247f5e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/InMemoryFactory.php @@ -24,7 +24,7 @@ */ class InMemoryFactory implements UserProviderFactoryInterface { - public function create(ContainerBuilder $container, $id, $config) + public function create(ContainerBuilder $container, string $id, array $config) { $definition = $container->setDefinition($id, new ChildDefinition('security.user.provider.in_memory')); $defaultPassword = new Parameter('container.build_id'); @@ -48,7 +48,7 @@ public function addConfiguration(NodeDefinition $node) ->fixXmlConfig('user') ->children() ->arrayNode('users') - ->useAttributeAsKey('name') + ->useAttributeAsKey('identifier') ->normalizeKeys(false) ->prototype('array') ->children() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php index 5007b740363f4..c2a334369ca0c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php @@ -24,7 +24,7 @@ */ class LdapFactory implements UserProviderFactoryInterface { - public function create(ContainerBuilder $container, $id, $config) + public function create(ContainerBuilder $container, string $id, array $config) { $container ->setDefinition($id, new ChildDefinition('security.user.provider.ldap')) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/UserProviderFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/UserProviderFactoryInterface.php index df250358aa61e..6d9481c59cb4a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/UserProviderFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/UserProviderFactoryInterface.php @@ -22,7 +22,7 @@ */ interface UserProviderFactoryInterface { - public function create(ContainerBuilder $container, $id, $config); + public function create(ContainerBuilder $container, string $id, array $config); public function getKey(); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 1996249a72070..aa3e246ad0e76 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -11,30 +11,41 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; +use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; -use Symfony\Bundle\SecurityBundle\SecurityUserValueResolver; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; +use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; +use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy; +use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy; +use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; -use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; -use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; +use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Http\Controller\UserValueResolver; -use Symfony\Component\Templating\Helper\Helper; -use Twig\Extension\AbstractExtension; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; /** * SecurityExtension. @@ -44,43 +55,20 @@ */ class SecurityExtension extends Extension implements PrependExtensionInterface { - private $requestMatchers = []; - private $expressions = []; - private $contextListeners = []; - private $listenerPositions = ['pre_auth', 'form', 'http', 'remember_me', 'anonymous']; - private $factories = []; - private $userProviderFactories = []; - private $statelessFirewallKeys = []; - - public function __construct() - { - foreach ($this->listenerPositions as $position) { - $this->factories[$position] = []; - } - } + private array $requestMatchers = []; + private array $expressions = []; + private array $contextListeners = []; + /** @var list */ + private array $factories = []; + /** @var AuthenticatorFactoryInterface[] */ + private array $sortedFactories = []; + private array $userProviderFactories = []; public function prepend(ContainerBuilder $container) { - $rememberMeSecureDefault = false; - $rememberMeSameSiteDefault = null; - - if (!isset($container->getExtensions()['framework'])) { - return; - } - foreach ($container->getExtensionConfig('framework') as $config) { - if (isset($config['session']) && \is_array($config['session'])) { - $rememberMeSecureDefault = $config['session']['cookie_secure'] ?? $rememberMeSecureDefault; - $rememberMeSameSiteDefault = \array_key_exists('cookie_samesite', $config['session']) ? $config['session']['cookie_samesite'] : $rememberMeSameSiteDefault; - } - } - foreach ($this->listenerPositions as $position) { - foreach ($this->factories[$position] as $factory) { - if ($factory instanceof RememberMeFactory) { - \Closure::bind(function () use ($rememberMeSecureDefault, $rememberMeSameSiteDefault) { - $this->options['secure'] = $rememberMeSecureDefault; - $this->options['samesite'] = $rememberMeSameSiteDefault; - }, $factory, $factory)(); - } + foreach ($this->getSortedFactories() as $factory) { + if ($factory instanceof PrependExtensionInterface) { + $factory->prepend($container); } } } @@ -96,27 +84,29 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->processConfiguration($mainConfig, $configs); // load services - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('security.xml'); - $loader->load('security_listeners.xml'); - $loader->load('security_rememberme.xml'); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); + + $loader->load('security.php'); + $loader->load('password_hasher.php'); + $loader->load('security_listeners.php'); - if (class_exists(Helper::class)) { - $loader->load('templating_php.xml'); + if (!$config['enable_authenticator_manager']) { + throw new InvalidConfigurationException('"security.enable_authenticator_manager" must be set to "true".'); } - if (class_exists(AbstractExtension::class)) { - $loader->load('templating_twig.xml'); + $loader->load('security_authenticator.php'); + + if ($container::willBeAvailable('symfony/twig-bridge', LogoutUrlExtension::class, ['symfony/security-bundle'])) { + $loader->load('templating_twig.php'); } - $loader->load('collectors.xml'); - $loader->load('guard.xml'); + $loader->load('collectors.php'); if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) { - $loader->load('security_debug.xml'); + $loader->load('security_debug.php'); } - if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) { + if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) { $container->removeDefinition('security.expression_language'); $container->removeDefinition('security.access.expression_voter'); } @@ -127,40 +117,67 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']); if (isset($config['access_decision_manager']['service'])) { - $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service'])->setPrivate(true); + $container->setAlias('security.access.decision_manager', $config['access_decision_manager']['service']); + } elseif (isset($config['access_decision_manager']['strategy_service'])) { + $container + ->getDefinition('security.access.decision_manager') + ->addArgument(new Reference($config['access_decision_manager']['strategy_service'])); } else { $container ->getDefinition('security.access.decision_manager') - ->addArgument($config['access_decision_manager']['strategy'] ?? AccessDecisionManager::STRATEGY_AFFIRMATIVE) - ->addArgument($config['access_decision_manager']['allow_if_all_abstain']) - ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']); + ->addArgument($this->createStrategyDefinition( + $config['access_decision_manager']['strategy'] ?? MainConfiguration::STRATEGY_AFFIRMATIVE, + $config['access_decision_manager']['allow_if_all_abstain'], + $config['access_decision_manager']['allow_if_equal_granted_denied'] + )); } - $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); + if (class_exists(Application::class)) { + $loader->load('debug_console.php'); + } + $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); - $container->getDefinition('security.authentication.guard_handler') - ->replaceArgument(2, $this->statelessFirewallKeys); - - if ($config['encoders']) { - $this->createEncoders($config['encoders'], $container); + if ($config['password_hashers']) { + $this->createHashers($config['password_hashers'], $container); } if (class_exists(Application::class)) { - $loader->load('console.xml'); - $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders'])); - } + $loader->load('console.php'); - if (!class_exists(UserValueResolver::class)) { - $container->getDefinition('security.user_value_resolver')->setClass(SecurityUserValueResolver::class); + $container->getDefinition('security.command.user_password_hash')->replaceArgument(1, array_keys($config['password_hashers'])); } $container->registerForAutoconfiguration(VoterInterface::class) ->addTag('security.voter'); + + // required for compatibility with Symfony 5.4 + $container->getDefinition('security.access_listener')->setArgument(3, false); + $container->getDefinition('security.authorization_checker')->setArgument(2, false); + $container->getDefinition('security.authorization_checker')->setArgument(3, false); + } + + /** + * @throws \InvalidArgumentException if the $strategy is invalid + */ + private function createStrategyDefinition(string $strategy, bool $allowIfAllAbstainDecisions, bool $allowIfEqualGrantedDeniedDecisions): Definition + { + switch ($strategy) { + case MainConfiguration::STRATEGY_AFFIRMATIVE: + return new Definition(AffirmativeStrategy::class, [$allowIfAllAbstainDecisions]); + case MainConfiguration::STRATEGY_CONSENSUS: + return new Definition(ConsensusStrategy::class, [$allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions]); + case MainConfiguration::STRATEGY_UNANIMOUS: + return new Definition(UnanimousStrategy::class, [$allowIfAllAbstainDecisions]); + case MainConfiguration::STRATEGY_PRIORITY: + return new Definition(PriorityStrategy::class, [$allowIfAllAbstainDecisions]); + } + + throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy)); } private function createRoleHierarchy(array $config, ContainerBuilder $container) @@ -220,6 +237,8 @@ private function createFirewalls(array $config, ContainerBuilder $container) $firewalls = $config['firewalls']; $providerIds = $this->createUserProviders($config, $container); + $container->setParameter('security.firewalls', array_keys($firewalls)); + // make the ContextListener aware of the configured user providers $contextListenerDefinition = $container->getDefinition('security.context_listener'); $arguments = $contextListenerDefinition->getArguments(); @@ -227,8 +246,18 @@ private function createFirewalls(array $config, ContainerBuilder $container) foreach ($providerIds as $userProviderId) { $userProviders[] = new Reference($userProviderId); } - $arguments[1] = new IteratorArgument($userProviders); + $arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders); $contextListenerDefinition->setArguments($arguments); + $nbUserProviders = \count($userProviders); + + if ($nbUserProviders > 1) { + $container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument])) + ->setPublic(false); + } elseif (0 === $nbUserProviders) { + $container->removeDefinition('security.listener.user_provider'); + } else { + $container->setAlias('security.user_providers', new Alias(current($providerIds)))->setPublic(false); + } if (1 === \count($providerIds)) { $container->setAlias(UserProviderInterface::class, current($providerIds)); @@ -249,7 +278,8 @@ private function createFirewalls(array $config, ContainerBuilder $container) [$matcher, $listeners, $exceptionListener, $logoutListener] = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); $contextId = 'security.firewall.map.context.'.$name; - $context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context'); + $isLazy = !$firewall['stateless'] && (!empty($firewall['anonymous']['lazy']) || $firewall['lazy']); + $context = new ChildDefinition($isLazy ? 'security.firewall.lazy_context' : 'security.firewall.context'); $context = $container->setDefinition($contextId, $context); $context ->replaceArgument(0, new IteratorArgument($listeners)) @@ -261,17 +291,11 @@ private function createFirewalls(array $config, ContainerBuilder $container) $contextRefs[$contextId] = new Reference($contextId); $map[$contextId] = $matcher; } - $mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs)); - $mapDef->replaceArgument(1, new IteratorArgument($map)); - // add authentication providers to authentication manager - $authenticationProviders = array_map(function ($id) { - return new Reference($id); - }, array_values(array_unique($authenticationProviders))); - $container - ->getDefinition('security.authentication.manager') - ->replaceArgument(0, new IteratorArgument($authenticationProviders)) - ; + $container->setAlias('security.firewall.context_locator', (string) ServiceLocatorTagPass::register($container, $contextRefs)); + + $mapDef->replaceArgument(0, new Reference('security.firewall.context_locator')); + $mapDef->replaceArgument(1, new IteratorArgument($map)); // register an autowire alias for the UserCheckerInterface if no custom user checker service is configured if (!$customUserChecker) { @@ -306,6 +330,8 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $config->replaceArgument(4, $firewall['stateless']); + $firewallEventDispatcherId = 'security.event_dispatcher.'.$id; + // Provider id (must be configured explicitly per firewall/authenticator if more than one provider is set) $defaultProvider = null; if (isset($firewall['provider'])) { @@ -313,12 +339,20 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall['provider'])); } $defaultProvider = $providerIds[$normalizedName]; + + $container->setDefinition('security.listener.'.$id.'.user_provider', new ChildDefinition('security.listener.user_provider.abstract')) + ->addTag('kernel.event_listener', ['dispatcher' => $firewallEventDispatcherId, 'event' => CheckPassportEvent::class, 'priority' => 2048, 'method' => 'checkPassport']) + ->replaceArgument(0, new Reference($defaultProvider)); } elseif (1 === \count($providerIds)) { $defaultProvider = reset($providerIds); } $config->replaceArgument(5, $defaultProvider); + // Register Firewall-specific event dispatcher + $container->register($firewallEventDispatcherId, EventDispatcher::class) + ->addTag('event_dispatcher.dispatcher', ['name' => $firewallEventDispatcherId]); + // Register listeners $listeners = []; $listenerKeys = []; @@ -331,10 +365,13 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ // Context serializer listener if (false === $firewall['stateless']) { $contextKey = $firewall['context'] ?? $id; - $listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey)); + $listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey, $firewallEventDispatcherId)); $sessionStrategyId = 'security.authentication.session_strategy'; + + $container + ->setDefinition('security.listener.session.'.$id, new ChildDefinition('security.listener.session')) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } else { - $this->statelessFirewallKeys[] = $id; $sessionStrategyId = 'security.authentication.session_strategy_noop'; } $container->setAlias(new Alias('security.authentication.session_strategy.'.$id, false), $sessionStrategyId); @@ -346,44 +383,34 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ if (isset($firewall['logout'])) { $logoutListenerId = 'security.logout_listener.'.$id; $logoutListener = $container->setDefinition($logoutListenerId, new ChildDefinition('security.logout_listener')); + $logoutListener->replaceArgument(2, new Reference($firewallEventDispatcherId)); $logoutListener->replaceArgument(3, [ 'csrf_parameter' => $firewall['logout']['csrf_parameter'], 'csrf_token_id' => $firewall['logout']['csrf_token_id'], 'logout_path' => $firewall['logout']['path'], ]); - // add logout success handler - if (isset($firewall['logout']['success_handler'])) { - $logoutSuccessHandlerId = $firewall['logout']['success_handler']; - } else { - $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; - $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new ChildDefinition('security.logout.success_handler')); - $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']); - } - $logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); + $logoutSuccessListenerId = 'security.logout.listener.default.'.$id; + $container->setDefinition($logoutSuccessListenerId, new ChildDefinition('security.logout.listener.default')) + ->replaceArgument(1, $firewall['logout']['target']) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); // add CSRF provider if (isset($firewall['logout']['csrf_token_generator'])) { $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } - // add session logout handler + // add session logout listener if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { - $logoutListener->addMethodCall('addHandler', [new Reference('security.logout.handler.session')]); + $container->setDefinition('security.logout.listener.session.'.$id, new ChildDefinition('security.logout.listener.session')) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } - // add cookie logout handler + // add cookie logout listener if (\count($firewall['logout']['delete_cookies']) > 0) { - $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; - $cookieHandler = $container->setDefinition($cookieHandlerId, new ChildDefinition('security.logout.handler.cookie_clearing')); - $cookieHandler->addArgument($firewall['logout']['delete_cookies']); - - $logoutListener->addMethodCall('addHandler', [new Reference($cookieHandlerId)]); - } - - // add custom handlers - foreach ($firewall['logout']['handlers'] as $handlerId) { - $logoutListener->addMethodCall('addHandler', [new Reference($handlerId)]); + $container->setDefinition('security.logout.listener.cookie_clearing.'.$id, new ChildDefinition('security.logout.listener.cookie_clearing')) + ->addArgument($firewall['logout']['delete_cookies']) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); } // register with LogoutUrlGenerator @@ -404,7 +431,55 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $configuredEntryPoint = $firewall['entry_point'] ?? null; // Authentication listeners - [$authListeners, $defaultEntryPoint] = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); + $firewallAuthenticationProviders = []; + [$authListeners, $defaultEntryPoint] = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); + + // $configuredEntryPoint is resolved into a service ID and stored in $defaultEntryPoint + $configuredEntryPoint = $defaultEntryPoint; + + // authenticator manager + $authenticators = array_map(function ($id) { + return new Reference($id); + }, $firewallAuthenticationProviders); + $container + ->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager')) + ->replaceArgument(0, $authenticators) + ->replaceArgument(2, new Reference($firewallEventDispatcherId)) + ->replaceArgument(3, $id) + ->replaceArgument(7, $firewall['required_badges'] ?? []) + ->addTag('monolog.logger', ['channel' => 'security']) + ; + + $managerLocator = $container->getDefinition('security.authenticator.managers_locator'); + $managerLocator->replaceArgument(0, array_merge($managerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference($managerId))])); + + // authenticator manager listener + $container + ->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator')) + ->replaceArgument(0, new Reference($managerId)) + ; + + if ($container->hasDefinition('debug.security.firewall')) { + $container + ->register('debug.security.firewall.authenticator.'.$id, TraceableAuthenticatorManagerListener::class) + ->setDecoratedService('security.firewall.authenticator.'.$id) + ->setArguments([new Reference('debug.security.firewall.authenticator.'.$id.'.inner')]) + ; + } + + // user checker listener + $container + ->setDefinition('security.listener.user_checker.'.$id, new ChildDefinition('security.listener.user_checker')) + ->replaceArgument(0, new Reference('security.user_checker.'.$id)) + ->addTag('kernel.event_subscriber', ['dispatcher' => $firewallEventDispatcherId]); + + $listeners[] = new Reference('security.firewall.authenticator.'.$id); + + // Add authenticators to the debug:firewall command + if ($container->hasDefinition('security.command.debug_firewall')) { + $debugCommand = $container->getDefinition('security.command.debug_firewall'); + $debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators])); + } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); @@ -427,12 +502,16 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $container->setAlias('security.user_checker.'.$id, new Alias($firewall['user_checker'], false)); - foreach ($this->factories as $position) { - foreach ($position as $factory) { - $key = str_replace('-', '_', $factory->getKey()); - if (\array_key_exists($key, $firewall)) { - $listenerKeys[] = $key; - } + foreach ($this->getSortedFactories() as $factory) { + $key = str_replace('-', '_', $factory->getKey()); + if ('custom_authenticators' !== $key && \array_key_exists($key, $firewall)) { + $listenerKeys[] = $key; + } + } + + if ($firewall['custom_authenticators'] ?? false) { + foreach ($firewall['custom_authenticators'] as $customAuthenticatorId) { + $listenerKeys[] = $customAuthenticatorId; } } @@ -442,7 +521,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null]; } - private function createContextListener(ContainerBuilder $container, string $contextKey) + private function createContextListener(ContainerBuilder $container, string $contextKey, ?string $firewallEventDispatcherId) { if (isset($this->contextListeners[$contextKey])) { return $this->contextListeners[$contextKey]; @@ -451,6 +530,10 @@ private function createContextListener(ContainerBuilder $container, string $cont $listenerId = 'security.context_listener.'.\count($this->contextListeners); $listener = $container->setDefinition($listenerId, new ChildDefinition('security.context_listener')); $listener->replaceArgument(2, $contextKey); + if (null !== $firewallEventDispatcherId) { + $listener->replaceArgument(4, new Reference($firewallEventDispatcherId)); + $listener->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE, 'method' => 'onKernelResponse']); + } return $this->contextListeners[$contextKey] = $listenerId; } @@ -458,66 +541,99 @@ private function createContextListener(ContainerBuilder $container, string $cont private function createAuthenticationListeners(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, ?string $defaultProvider, array $providerIds, ?string $defaultEntryPoint, string $contextListenerId = null) { $listeners = []; - $hasListeners = false; - - foreach ($this->listenerPositions as $position) { - foreach ($this->factories[$position] as $factory) { - $key = str_replace('-', '_', $factory->getKey()); - - if (isset($firewall[$key])) { - if (isset($firewall[$key]['provider'])) { - if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$key]['provider'])])) { - throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider'])); - } - $userProvider = $providerIds[$normalizedName]; - } elseif ('remember_me' === $key || 'anonymous' === $key) { - // RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users. - $userProvider = null; - - if ('remember_me' === $key && $contextListenerId) { - $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); - } - } elseif ($defaultProvider) { - $userProvider = $defaultProvider; - } elseif (empty($providerIds)) { - $userProvider = sprintf('security.user.provider.missing.%s', $key); - $container->setDefinition($userProvider, (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)); - } else { - throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $key, $id)); - } + $entryPoints = []; + + foreach ($this->getSortedFactories() as $factory) { + $key = str_replace('-', '_', $factory->getKey()); - [$provider, $listenerId, $defaultEntryPoint] = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); + if (isset($firewall[$key])) { + $userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId); - $listeners[] = new Reference($listenerId); - $authenticationProviders[] = $provider; - $hasListeners = true; + if (!$factory instanceof AuthenticatorFactoryInterface) { + throw new InvalidConfigurationException(sprintf('Authenticator factory "%s" ("%s") must implement "%s".', get_debug_type($factory), $key, AuthenticatorFactoryInterface::class)); + } + + $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); + if (\is_array($authenticators)) { + foreach ($authenticators as $authenticator) { + $authenticationProviders[] = $authenticator; + $entryPoints[] = $authenticator; + } + } else { + $authenticationProviders[] = $authenticators; + $entryPoints[$key] = $authenticators; + } + + if ($factory instanceof FirewallListenerFactoryInterface) { + $firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]); + foreach ($firewallListenerIds as $firewallListenerId) { + $listeners[] = new Reference($firewallListenerId); + } } } } - if (false === $hasListeners) { - throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id)); - } + // the actual entry point is configured by the RegisterEntryPointPass + $container->setParameter('security.'.$id.'._indexed_authenticators', $entryPoints); return [$listeners, $defaultEntryPoint]; } - private function createEncoders(array $encoders, ContainerBuilder $container) + private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string + { + if (isset($firewall[$factoryKey]['provider'])) { + if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) { + throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$factoryKey]['provider'])); + } + + return $providerIds[$normalizedName]; + } + + if ('remember_me' === $factoryKey && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + } + + if ($defaultProvider) { + return $defaultProvider; + } + + if (!$providerIds) { + $userProvider = sprintf('security.user.provider.missing.%s', $factoryKey); + $container->setDefinition( + $userProvider, + (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id) + ); + + return $userProvider; + } + + if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey || 'custom_authenticators' === $factoryKey) { + if ('custom_authenticators' === $factoryKey) { + trigger_deprecation('symfony/security-bundle', '5.4', 'Not configuring explicitly the provider for the "%s" firewall is deprecated because it\'s ambiguous as there is more than one registered provider. Set the "provider" key to one of the configured providers, even if your custom authenticators don\'t use it.', $id); + } + + return 'security.user_providers'; + } + + throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" authenticator on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id)); + } + + private function createHashers(array $hashers, ContainerBuilder $container) { - $encoderMap = []; - foreach ($encoders as $class => $encoder) { - $encoderMap[$class] = $this->createEncoder($encoder); + $hasherMap = []; + foreach ($hashers as $class => $hasher) { + $hasherMap[$class] = $this->createHasher($hasher); } $container - ->getDefinition('security.encoder_factory.generic') - ->setArguments([$encoderMap]) + ->getDefinition('security.password_hasher_factory') + ->setArguments([$hasherMap]) ; } - private function createEncoder(array $config) + private function createHasher(array $config) { - // a custom encoder service + // a custom hasher service if (isset($config['id'])) { return new Reference($config['id']); } @@ -526,20 +642,20 @@ private function createEncoder(array $config) return $config; } - // plaintext encoder + // plaintext hasher if ('plaintext' === $config['algorithm']) { $arguments = [$config['ignore_case']]; return [ - 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'class' => PlaintextPasswordHasher::class, 'arguments' => $arguments, ]; } - // pbkdf2 encoder + // pbkdf2 hasher if ('pbkdf2' === $config['algorithm']) { return [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'class' => Pbkdf2PasswordHasher::class, 'arguments' => [ $config['hash_algorithm'], $config['encode_as_base64'], @@ -549,17 +665,17 @@ private function createEncoder(array $config) ]; } - // bcrypt encoder + // bcrypt hasher if ('bcrypt' === $config['algorithm']) { $config['algorithm'] = 'native'; $config['native_algorithm'] = \PASSWORD_BCRYPT; - return $this->createEncoder($config); + return $this->createHasher($config); } - // Argon2i encoder + // Argon2i hasher if ('argon2i' === $config['algorithm']) { - if (SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + if (SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $config['algorithm'] = 'sodium'; } elseif (\defined('PASSWORD_ARGON2I')) { $config['algorithm'] = 'native'; @@ -568,11 +684,11 @@ private function createEncoder(array $config) throw new InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use "%s" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto')); } - return $this->createEncoder($config); + return $this->createHasher($config); } if ('argon2id' === $config['algorithm']) { - if (($hasSodium = SodiumPasswordEncoder::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { + if (($hasSodium = SodiumPasswordHasher::isSupported()) && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) { $config['algorithm'] = 'sodium'; } elseif (\defined('PASSWORD_ARGON2ID')) { $config['algorithm'] = 'native'; @@ -581,12 +697,12 @@ private function createEncoder(array $config) throw new InvalidConfigurationException(sprintf('Algorithm "argon2id" is not available. Either use "%s", upgrade to PHP 7.3+ or use libsodium 1.0.15+ instead.', \defined('PASSWORD_ARGON2I') || $hasSodium ? 'argon2i", "auto' : 'auto')); } - return $this->createEncoder($config); + return $this->createHasher($config); } if ('native' === $config['algorithm']) { return [ - 'class' => NativePasswordEncoder::class, + 'class' => NativePasswordHasher::class, 'arguments' => [ $config['time_cost'], (($config['memory_cost'] ?? 0) << 10) ?: null, @@ -596,12 +712,12 @@ private function createEncoder(array $config) } if ('sodium' === $config['algorithm']) { - if (!SodiumPasswordEncoder::isSupported()) { + if (!SodiumPasswordHasher::isSupported()) { throw new InvalidConfigurationException('Libsodium is not available. Install the sodium extension or use "auto" instead.'); } return [ - 'class' => SodiumPasswordEncoder::class, + 'class' => SodiumPasswordHasher::class, 'arguments' => [ $config['time_cost'], (($config['memory_cost'] ?? 0) << 10) ?: null, @@ -609,7 +725,7 @@ private function createEncoder(array $config) ]; } - // run-time configured encoder + // run-time configured hasher return $config; } @@ -703,7 +819,7 @@ private function createSwitchUserListener(ContainerBuilder $container, string $i $listener->replaceArgument(3, $id); $listener->replaceArgument(6, $config['parameter']); $listener->replaceArgument(7, $config['role']); - $listener->replaceArgument(9, $stateless ?: $config['stateless']); + $listener->replaceArgument(9, $stateless); return $switchUserListenerId; } @@ -714,8 +830,8 @@ private function createExpression(ContainerBuilder $container, string $expressio return $this->expressions[$id]; } - if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) { - throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); + if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) { + throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); } $container @@ -737,7 +853,7 @@ private function createRequestMatcher(ContainerBuilder $container, string $path foreach ($ips as $ip) { $container->resolveEnvPlaceholders($ip, null, $usedEnvs); - if (!$usedEnvs && !$this->isValidIp($ip)) { + if (!$usedEnvs && !$this->isValidIps($ip)) { throw new \LogicException(sprintf('The given value "%s" in the "security.access_control" config option is not a valid IP address.', $ip)); } @@ -766,9 +882,10 @@ private function createRequestMatcher(ContainerBuilder $container, string $path return $this->requestMatchers[$id] = new Reference($id); } - public function addSecurityListenerFactory(SecurityFactoryInterface $factory) + public function addAuthenticatorFactory(AuthenticatorFactoryInterface $factory) { - $this->factories[$factory->getPosition()][] = $factory; + $this->factories[] = [$factory->getPriority(), $factory]; + $this->sortedFactories = []; } public function addUserProviderFactory(UserProviderFactoryInterface $factory) @@ -779,20 +896,39 @@ public function addUserProviderFactory(UserProviderFactoryInterface $factory) /** * {@inheritdoc} */ - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string|false { return __DIR__.'/../Resources/config/schema'; } - public function getNamespace() + public function getNamespace(): string { return 'http://symfony.com/schema/dic/security'; } - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface { // first assemble the factories - return new MainConfiguration($this->factories, $this->userProviderFactories); + return new MainConfiguration($this->getSortedFactories(), $this->userProviderFactories); + } + + private function isValidIps(string|array $ips): bool + { + $ipsList = array_reduce((array) $ips, static function (array $ips, string $ip) { + return array_merge($ips, preg_split('/\s*,\s*/', $ip)); + }, []); + + if (!$ipsList) { + return false; + } + + foreach ($ipsList as $cidr) { + if (!$this->isValidIp($cidr)) { + return false; + } + } + + return true; } private function isValidIp(string $cidr): bool @@ -820,4 +956,25 @@ private function isValidIp(string $cidr): bool return false; } + + /** + * @return array + */ + private function getSortedFactories(): array + { + if (!$this->sortedFactories) { + $factories = []; + foreach ($this->factories as $i => $factory) { + $factories[] = array_merge($factory, [$i]); + } + + usort($factories, function ($a, $b) { + return $b[0] <=> $a[0] ?: $a[2] <=> $b[2]; + }); + + $this->sortedFactories = array_column($factories, 1); + } + + return $this->sortedFactories; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php index 3dca451d9ab17..414c5f12aec9f 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php @@ -12,13 +12,13 @@ namespace Symfony\Bundle\SecurityBundle\EventListener; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Security\Http\Firewall; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @author Maxime Steinhausser @@ -30,20 +30,15 @@ class FirewallListener extends Firewall public function __construct(FirewallMapInterface $map, EventDispatcherInterface $dispatcher, LogoutUrlGenerator $logoutUrlGenerator) { - // the type-hint will be updated to the "EventDispatcherInterface" from symfony/contracts in 5.0 - $this->map = $map; $this->logoutUrlGenerator = $logoutUrlGenerator; parent::__construct($map, $dispatcher); } - /** - * @internal - */ - public function configureLogoutUrlGenerator(GetResponseEvent $event) + public function configureLogoutUrlGenerator(RequestEvent $event) { - if (!$event->isMasterRequest()) { + if (!$event->isMainRequest()) { return; } @@ -52,12 +47,9 @@ public function configureLogoutUrlGenerator(GetResponseEvent $event) } } - /** - * @internal since Symfony 4.3 - */ public function onKernelFinishRequest(FinishRequestEvent $event) { - if ($event->isMasterRequest()) { + if ($event->isMainRequest()) { $this->logoutUrlGenerator->setCurrentFirewall(null); } @@ -67,7 +59,7 @@ public function onKernelFinishRequest(FinishRequestEvent $event) /** * {@inheritdoc} */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [ diff --git a/src/Symfony/Bundle/SecurityBundle/LoginLink/FirewallAwareLoginLinkHandler.php b/src/Symfony/Bundle/SecurityBundle/LoginLink/FirewallAwareLoginLinkHandler.php new file mode 100644 index 0000000000000..5c61cfcfabad4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/LoginLink/FirewallAwareLoginLinkHandler.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\LoginLink; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security\FirewallAwareTrait; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\LoginLink\LoginLinkDetails; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +/** + * Decorates the login link handler for the current firewall. + * + * @author Ryan Weaver + */ +class FirewallAwareLoginLinkHandler implements LoginLinkHandlerInterface +{ + use FirewallAwareTrait; + + private const FIREWALL_OPTION = 'login_link'; + + public function __construct(FirewallMap $firewallMap, ContainerInterface $loginLinkHandlerLocator, RequestStack $requestStack) + { + $this->firewallMap = $firewallMap; + $this->locator = $loginLinkHandlerLocator; + $this->requestStack = $requestStack; + } + + public function createLoginLink(UserInterface $user, Request $request = null): LoginLinkDetails + { + return $this->getForFirewall()->createLoginLink($user, $request); + } + + public function consumeLoginLink(Request $request): UserInterface + { + return $this->getForFirewall()->consumeLoginLink($request); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/RememberMe/DecoratedRememberMeHandler.php b/src/Symfony/Bundle/SecurityBundle/RememberMe/DecoratedRememberMeHandler.php new file mode 100644 index 0000000000000..a060fb5116ffb --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/RememberMe/DecoratedRememberMeHandler.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\RememberMe; + +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\RememberMe\RememberMeDetails; +use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; + +/** + * Used as a "workaround" for tagging aliases in the RememberMeFactory. + * + * @author Wouter de Jong + * + * @internal + */ +final class DecoratedRememberMeHandler implements RememberMeHandlerInterface +{ + private $handler; + + public function __construct(RememberMeHandlerInterface $handler) + { + $this->handler = $handler; + } + + /** + * {@inheritDoc} + */ + public function createRememberMeCookie(UserInterface $user): void + { + $this->handler->createRememberMeCookie($user); + } + + /** + * {@inheritDoc} + */ + public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface + { + return $this->handler->consumeRememberMeCookie($rememberMeDetails); + } + + /** + * {@inheritDoc} + */ + public function clearRememberMeCookie(): void + { + $this->handler->clearRememberMeCookie(); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/RememberMe/FirewallAwareRememberMeHandler.php b/src/Symfony/Bundle/SecurityBundle/RememberMe/FirewallAwareRememberMeHandler.php new file mode 100644 index 0000000000000..ca7450f2d9ab9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/RememberMe/FirewallAwareRememberMeHandler.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\RememberMe; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\Security\FirewallAwareTrait; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\RememberMe\RememberMeDetails; +use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; + +/** + * Decorates {@see RememberMeHandlerInterface} for the current firewall. + * + * @author Wouter de Jong + */ +final class FirewallAwareRememberMeHandler implements RememberMeHandlerInterface +{ + use FirewallAwareTrait; + + private const FIREWALL_OPTION = 'remember_me'; + + public function __construct(FirewallMap $firewallMap, ContainerInterface $rememberMeHandlerLocator, RequestStack $requestStack) + { + $this->firewallMap = $firewallMap; + $this->locator = $rememberMeHandlerLocator; + $this->requestStack = $requestStack; + } + + public function createRememberMeCookie(UserInterface $user): void + { + $this->getForFirewall()->createRememberMeCookie($user); + } + + public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface + { + return $this->getForFirewall()->consumeRememberMeCookie($rememberMeDetails); + } + + public function clearRememberMeCookie(): void + { + $this->getForFirewall()->clearRememberMeCookie(); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.php new file mode 100644 index 0000000000000..3619779311ae2 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('data_collector.security', SecurityDataCollector::class) + ->args([ + service('security.untracked_token_storage'), + service('security.role_hierarchy'), + service('security.logout_url_generator'), + service('security.access.decision_manager'), + service('security.firewall.map'), + service('debug.security.firewall')->nullOnInvalid(), + ]) + ->tag('data_collector', [ + 'template' => '@Security/Collector/security.html.twig', + 'id' => 'security', + 'priority' => 270, + ]) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml deleted file mode 100644 index 811c6dfc5cfdc..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.php new file mode 100644 index 0000000000000..5ab29caee1487 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.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\DependencyInjection\Loader\Configurator; + +use Symfony\Component\PasswordHasher\Command\UserPasswordHashCommand; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.command.user_password_hash', UserPasswordHashCommand::class) + ->args([ + service('security.password_hasher_factory'), + abstract_arg('list of user classes'), + ]) + ->tag('console.command') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml deleted file mode 100644 index 2e28eda8890fa..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/console.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php new file mode 100644 index 0000000000000..74fa434926063 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/debug_console.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\Command\DebugFirewallCommand; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.command.debug_firewall', DebugFirewallCommand::class) + ->args([ + param('security.firewalls'), + service('security.firewall.context_locator'), + tagged_locator('event_dispatcher.dispatcher', 'name'), + [], + false, + ]) + ->tag('console.command', ['command' => 'debug:firewall']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml deleted file mode 100644 index 2fae1438991cd..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.authentication.hide_user_not_found% - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/password_hasher.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/password_hasher.php new file mode 100644 index 0000000000000..50e1be8d981cd --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/password_hasher.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory; +use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.password_hasher_factory', PasswordHasherFactory::class) + ->args([[]]) + ->alias(PasswordHasherFactoryInterface::class, 'security.password_hasher_factory') + + ->set('security.user_password_hasher', UserPasswordHasher::class) + ->args([service('security.password_hasher_factory')]) + ->alias('security.password_hasher', 'security.user_password_hasher') + ->alias(UserPasswordHasherInterface::class, 'security.password_hasher') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd new file mode 100644 index 0000000000000..ccc41eda0c51b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -0,0 +1,399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php new file mode 100644 index 0000000000000..e655520b0e745 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -0,0 +1,263 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\CacheWarmer\ExpressionCacheWarmer; +use Symfony\Bundle\SecurityBundle\EventListener\FirewallListener; +use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; +use Symfony\Component\Ldap\Security\LdapUserProvider; +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; +use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; +use Symfony\Component\Security\Core\Role\RoleHierarchy; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\ChainUserProvider; +use Symfony\Component\Security\Core\User\InMemoryUserChecker; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\User\MissingUserProvider; +use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use Symfony\Component\Security\Http\Controller\UserValueResolver; +use Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Security\Http\FirewallMapInterface; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy; +use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; + +return static function (ContainerConfigurator $container) { + $container->parameters() + ->set('security.role_hierarchy.roles', []) + ; + + $container->services() + ->set('security.authorization_checker', AuthorizationChecker::class) + ->args([ + service('security.token_storage'), + service('security.access.decision_manager'), + ]) + ->alias(AuthorizationCheckerInterface::class, 'security.authorization_checker') + + ->set('security.token_storage', UsageTrackingTokenStorage::class) + ->args([ + service('security.untracked_token_storage'), + service_locator([ + 'request_stack' => service('request_stack'), + ]), + ]) + ->tag('kernel.reset', ['method' => 'disableUsageTracking']) + ->tag('kernel.reset', ['method' => 'setToken']) + ->alias(TokenStorageInterface::class, 'security.token_storage') + + ->set('security.untracked_token_storage', TokenStorage::class) + + ->set('security.helper', Security::class) + ->args([service_locator([ + 'security.token_storage' => service('security.token_storage'), + 'security.authorization_checker' => service('security.authorization_checker'), + ])]) + ->alias(Security::class, 'security.helper') + + ->set('security.user_value_resolver', UserValueResolver::class) + ->args([ + service('security.token_storage'), + ]) + ->tag('controller.argument_value_resolver', ['priority' => 40]) + + // Authentication related services + ->set('security.authentication.trust_resolver', AuthenticationTrustResolver::class) + + ->set('security.authentication.session_strategy', SessionAuthenticationStrategy::class) + ->args([param('security.authentication.session_strategy.strategy')]) + ->alias(SessionAuthenticationStrategyInterface::class, 'security.authentication.session_strategy') + + ->set('security.authentication.session_strategy_noop', SessionAuthenticationStrategy::class) + ->args(['none']) + + ->set('security.user_checker', InMemoryUserChecker::class) + + ->set('security.expression_language', ExpressionLanguage::class) + ->args([service('cache.security_expression_language')->nullOnInvalid()]) + + ->set('security.authentication_utils', AuthenticationUtils::class) + ->args([service('request_stack')]) + ->alias(AuthenticationUtils::class, 'security.authentication_utils') + + // Authorization related services + ->set('security.access.decision_manager', AccessDecisionManager::class) + ->args([[]]) + ->alias(AccessDecisionManagerInterface::class, 'security.access.decision_manager') + + ->set('security.role_hierarchy', RoleHierarchy::class) + ->args([param('security.role_hierarchy.roles')]) + ->alias(RoleHierarchyInterface::class, 'security.role_hierarchy') + + // Security Voters + ->set('security.access.simple_role_voter', RoleVoter::class) + ->tag('security.voter', ['priority' => 245]) + + ->set('security.access.authenticated_voter', AuthenticatedVoter::class) + ->args([service('security.authentication.trust_resolver')]) + ->tag('security.voter', ['priority' => 250]) + + ->set('security.access.role_hierarchy_voter', RoleHierarchyVoter::class) + ->args([service('security.role_hierarchy')]) + ->tag('security.voter', ['priority' => 245]) + + ->set('security.access.expression_voter', ExpressionVoter::class) + ->args([ + service('security.expression_language'), + service('security.authentication.trust_resolver'), + service('security.authorization_checker'), + service('security.role_hierarchy')->nullOnInvalid(), + ]) + ->tag('security.voter', ['priority' => 245]) + + ->set('security.impersonate_url_generator', ImpersonateUrlGenerator::class) + ->args([ + service('request_stack'), + service('security.firewall.map'), + service('security.token_storage'), + ]) + + // Firewall related services + ->set('security.firewall', FirewallListener::class) + ->args([ + service('security.firewall.map'), + service('event_dispatcher'), + service('security.logout_url_generator'), + ]) + ->tag('kernel.event_subscriber') + ->alias(Firewall::class, 'security.firewall') + + ->set('security.firewall.map', FirewallMap::class) + ->args([ + abstract_arg('Firewall context locator'), + abstract_arg('Request matchers'), + ]) + ->alias(FirewallMapInterface::class, 'security.firewall.map') + + ->set('security.firewall.context', FirewallContext::class) + ->abstract() + ->args([ + [], + service('security.exception_listener'), + abstract_arg('LogoutListener'), + abstract_arg('FirewallConfig'), + ]) + + ->set('security.firewall.lazy_context', LazyFirewallContext::class) + ->abstract() + ->args([ + [], + service('security.exception_listener'), + abstract_arg('LogoutListener'), + abstract_arg('FirewallConfig'), + service('security.untracked_token_storage'), + ]) + + ->set('security.firewall.config', FirewallConfig::class) + ->abstract() + ->args([ + abstract_arg('name'), + abstract_arg('user_checker'), + abstract_arg('request_matcher'), + false, // security enabled + false, // stateless + null, + null, + null, + null, + null, + [], // listeners + null, // switch_user + ]) + + ->set('security.logout_url_generator', LogoutUrlGenerator::class) + ->args([ + service('request_stack')->nullOnInvalid(), + service('router')->nullOnInvalid(), + service('security.token_storage')->nullOnInvalid(), + ]) + + // Provisioning + ->set('security.user.provider.missing', MissingUserProvider::class) + ->abstract() + ->args([ + abstract_arg('firewall'), + ]) + + ->set('security.user.provider.in_memory', InMemoryUserProvider::class) + ->abstract() + + ->set('security.user.provider.ldap', LdapUserProvider::class) + ->abstract() + ->args([ + abstract_arg('security.ldap.ldap'), + abstract_arg('base dn'), + abstract_arg('search dn'), + abstract_arg('search password'), + abstract_arg('default_roles'), + abstract_arg('uid key'), + abstract_arg('filter'), + abstract_arg('password_attribute'), + abstract_arg('extra_fields (email etc)'), + ]) + + ->set('security.user.provider.chain', ChainUserProvider::class) + ->abstract() + + ->set('security.http_utils', HttpUtils::class) + ->args([ + service('router')->nullOnInvalid(), + service('router')->nullOnInvalid(), + ]) + ->alias(HttpUtils::class, 'security.http_utils') + + // Validator + ->set('security.validator.user_password', UserPasswordValidator::class) + ->args([ + service('security.token_storage'), + service('security.password_hasher_factory'), + ]) + ->tag('validator.constraint_validator', ['alias' => 'security.validator.user_password']) + + // Cache + ->set('cache.security_expression_language') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + + // Cache Warmers + ->set('security.cache_warmer.expression', ExpressionCacheWarmer::class) + ->args([ + [], + service('security.expression_language'), + ]) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml deleted file mode 100644 index 3491383b8bba6..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - null - null - - - - - - - - - - - %security.access.always_authenticate_before_granting% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.authentication.manager.erase_credentials% - - - - - - - - %security.authentication.trust_resolver.anonymous_class% - %security.authentication.trust_resolver.rememberme_class% - - - - %security.authentication.session_strategy.strategy% - - - - - none - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.role_hierarchy.roles% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - false - - - - - - - null - - - - - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.1. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php new file mode 100644 index 0000000000000..58be697595d42 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\Security\UserAuthenticator; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; +use Symfony\Component\Security\Http\Authenticator\X509Authenticator; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener; +use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener; +use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener; +use Symfony\Component\Security\Http\EventListener\SessionStrategyListener; +use Symfony\Component\Security\Http\EventListener\UserCheckerListener; +use Symfony\Component\Security\Http\EventListener\UserProviderListener; +use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; + +return static function (ContainerConfigurator $container) { + $container->services() + + // Manager + ->set('security.authenticator.manager', AuthenticatorManager::class) + ->abstract() + ->args([ + abstract_arg('authenticators'), + service('security.token_storage'), + service('event_dispatcher'), + abstract_arg('provider key'), + service('logger')->nullOnInvalid(), + param('security.authentication.manager.erase_credentials'), + param('security.authentication.hide_user_not_found'), + abstract_arg('required badges'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.managers_locator', ServiceLocator::class) + ->args([[]]) + + ->set('security.user_authenticator', UserAuthenticator::class) + ->args([ + service('security.firewall.map'), + service('security.authenticator.managers_locator'), + service('request_stack'), + ]) + ->alias(UserAuthenticatorInterface::class, 'security.user_authenticator') + + ->set('security.firewall.authenticator', AuthenticatorManagerListener::class) + ->abstract() + ->args([ + abstract_arg('authenticator manager'), + ]) + + // Listeners + ->set('security.listener.check_authenticator_credentials', CheckCredentialsListener::class) + ->args([ + service('security.password_hasher_factory'), + ]) + ->tag('kernel.event_subscriber') + + ->set('security.listener.user_provider', UserProviderListener::class) + ->args([ + service('security.user_providers'), + ]) + ->tag('kernel.event_listener', ['event' => CheckPassportEvent::class, 'priority' => 1024, 'method' => 'checkPassport']) + + ->set('security.listener.user_provider.abstract', UserProviderListener::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + ]) + + ->set('security.listener.password_migrating', PasswordMigratingListener::class) + ->args([ + service('security.password_hasher_factory'), + ]) + ->tag('kernel.event_subscriber') + + ->set('security.listener.user_checker', UserCheckerListener::class) + ->abstract() + ->args([ + abstract_arg('user checker'), + ]) + + ->set('security.listener.session', SessionStrategyListener::class) + ->abstract() + ->args([ + service('security.authentication.session_strategy'), + ]) + + ->set('security.listener.login_throttling', LoginThrottlingListener::class) + ->abstract() + ->args([ + service('request_stack'), + abstract_arg('request rate limiter'), + ]) + + // Authenticators + ->set('security.authenticator.http_basic', HttpBasicAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('realm name'), + abstract_arg('user provider'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.form_login', FormLoginAuthenticator::class) + ->abstract() + ->args([ + service('security.http_utils'), + abstract_arg('user provider'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + ]) + + ->set('security.authenticator.json_login', JsonLoginAuthenticator::class) + ->abstract() + ->args([ + service('security.http_utils'), + abstract_arg('user provider'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + service('property_accessor')->nullOnInvalid(), + ]) + ->call('setTranslator', [service('translator')->ignoreOnInvalid()]) + + ->set('security.authenticator.x509', X509Authenticator::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + service('security.token_storage'), + abstract_arg('firewall name'), + abstract_arg('user key'), + abstract_arg('credentials key'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.remote_user', RemoteUserAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('user provider'), + service('security.token_storage'), + abstract_arg('firewall name'), + abstract_arg('user key'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php new file mode 100644 index 0000000000000..9a46a0926dda9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\LoginLink\FirewallAwareLoginLinkHandler; +use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage; +use Symfony\Component\Security\Core\Signature\SignatureHasher; +use Symfony\Component\Security\Http\Authenticator\LoginLinkAuthenticator; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.authenticator.login_link', LoginLinkAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('the login link handler instance'), + service('security.http_utils'), + abstract_arg('authentication success handler'), + abstract_arg('authentication failure handler'), + abstract_arg('options'), + ]) + + ->set('security.authenticator.abstract_login_link_handler', LoginLinkHandler::class) + ->abstract() + ->args([ + service('router'), + abstract_arg('user provider'), + abstract_arg('signature hasher'), + abstract_arg('options'), + ]) + + ->set('security.authenticator.abstract_login_link_signature_hasher', SignatureHasher::class) + ->args([ + service('property_accessor'), + abstract_arg('signature properties'), + '%kernel.secret%', + abstract_arg('expired signature storage'), + abstract_arg('max signature uses'), + ]) + + ->set('security.authenticator.expired_login_link_storage', ExpiredSignatureStorage::class) + ->abstract() + ->args([ + abstract_arg('cache pool service'), + abstract_arg('expired login link storage'), + ]) + + ->set('security.authenticator.cache.expired_links') + ->parent('cache.app') + ->private() + + ->set('security.authenticator.firewall_aware_login_link_handler', FirewallAwareLoginLinkHandler::class) + ->args([ + service('security.firewall.map'), + tagged_locator('security.authenticator.login_linker', 'firewall'), + service('request_stack'), + ]) + ->alias(LoginLinkHandlerInterface::class, 'security.authenticator.firewall_aware_login_link_handler') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_remember_me.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_remember_me.php new file mode 100644 index 0000000000000..8304ed9b832da --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_remember_me.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\RememberMe\FirewallAwareRememberMeHandler; +use Symfony\Component\Security\Core\Signature\SignatureHasher; +use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; +use Symfony\Component\Security\Http\EventListener\CheckRememberMeConditionsListener; +use Symfony\Component\Security\Http\EventListener\RememberMeListener; +use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler; +use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface; +use Symfony\Component\Security\Http\RememberMe\ResponseListener; +use Symfony\Component\Security\Http\RememberMe\SignatureRememberMeHandler; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('security.rememberme.response_listener', ResponseListener::class) + ->tag('kernel.event_subscriber') + + ->set('security.authenticator.remember_me_signature_hasher', SignatureHasher::class) + ->args([ + service('property_accessor'), + abstract_arg('signature properties'), + '%kernel.secret%', + null, + null, + ]) + + ->set('security.authenticator.signature_remember_me_handler', SignatureRememberMeHandler::class) + ->abstract() + ->args([ + abstract_arg('signature hasher'), + abstract_arg('user provider'), + service('request_stack'), + abstract_arg('options'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.persistent_remember_me_handler', PersistentRememberMeHandler::class) + ->abstract() + ->args([ + abstract_arg('token provider'), + param('kernel.secret'), + abstract_arg('user provider'), + service('request_stack'), + abstract_arg('options'), + service('logger')->nullOnInvalid(), + abstract_arg('token verifier'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.firewall_aware_remember_me_handler', FirewallAwareRememberMeHandler::class) + ->args([ + service('security.firewall.map'), + tagged_locator('security.remember_me_handler', 'firewall'), + service('request_stack'), + ]) + ->alias(RememberMeHandlerInterface::class, 'security.authenticator.firewall_aware_remember_me_handler') + + ->set('security.listener.check_remember_me_conditions', CheckRememberMeConditionsListener::class) + ->abstract() + ->args([ + abstract_arg('options'), + service('logger')->nullOnInvalid(), + ]) + + ->set('security.listener.remember_me', RememberMeListener::class) + ->abstract() + ->args([ + abstract_arg('remember me handler'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authenticator.remember_me', RememberMeAuthenticator::class) + ->abstract() + ->args([ + abstract_arg('remember me handler'), + param('kernel.secret'), + service('security.token_storage'), + abstract_arg('options'), + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + // Cache + ->set('cache.security_token_verifier') + ->parent('cache.system') + ->private() + ->tag('cache.pool') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.php new file mode 100644 index 0000000000000..dc668b15e9ded --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; +use Symfony\Bundle\SecurityBundle\EventListener\VoteListener; +use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('debug.security.access.decision_manager', TraceableAccessDecisionManager::class) + ->decorate('security.access.decision_manager') + ->args([ + service('debug.security.access.decision_manager.inner'), + ]) + + ->set('debug.security.voter.vote_listener', VoteListener::class) + ->args([ + service('debug.security.access.decision_manager'), + ]) + ->tag('kernel.event_subscriber') + + ->set('debug.security.firewall', TraceableFirewallListener::class) + ->args([ + service('security.firewall.map'), + service('event_dispatcher'), + service('security.logout_url_generator'), + ]) + ->tag('kernel.event_subscriber') + ->alias('security.firewall', 'debug.security.firewall') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml deleted file mode 100644 index e348cb8b7406a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php new file mode 100644 index 0000000000000..2bbe4caa39c5a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\Security\Http\AccessMap; +use Symfony\Component\Security\Http\Authentication\CustomAuthenticationFailureHandler; +use Symfony\Component\Security\Http\Authentication\CustomAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; +use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; +use Symfony\Component\Security\Http\EventListener\CookieClearingLogoutListener; +use Symfony\Component\Security\Http\EventListener\DefaultLogoutListener; +use Symfony\Component\Security\Http\EventListener\SessionLogoutListener; +use Symfony\Component\Security\Http\Firewall\AccessListener; +use Symfony\Component\Security\Http\Firewall\ChannelListener; +use Symfony\Component\Security\Http\Firewall\ContextListener; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\LogoutListener; +use Symfony\Component\Security\Http\Firewall\SwitchUserListener; + +return static function (ContainerConfigurator $container) { + $container->services() + + ->set('security.channel_listener', ChannelListener::class) + ->args([ + service('security.access_map'), + service('logger')->nullOnInvalid(), + inline_service('int')->factory([service('router.request_context'), 'getHttpPort']), + inline_service('int')->factory([service('router.request_context'), 'getHttpsPort']), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.access_map', AccessMap::class) + + ->set('security.context_listener', ContextListener::class) + ->args([ + service('security.untracked_token_storage'), + [], + abstract_arg('Provider Key'), + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + service('security.authentication.trust_resolver'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.logout_listener', LogoutListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.http_utils'), + abstract_arg('event dispatcher'), + [], // Options + ]) + + ->set('security.logout.listener.session', SessionLogoutListener::class) + ->abstract() + + ->set('security.logout.listener.cookie_clearing', CookieClearingLogoutListener::class) + ->abstract() + + ->set('security.logout.listener.default', DefaultLogoutListener::class) + ->abstract() + ->args([ + service('security.http_utils'), + abstract_arg('target url'), + ]) + + ->set('security.authentication.listener.abstract') + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.manager'), + service('security.authentication.session_strategy'), + service('security.http_utils'), + abstract_arg('Provider-shared Key'), + service('security.authentication.success_handler'), + service('security.authentication.failure_handler'), + [], + service('logger')->nullOnInvalid(), + service('event_dispatcher')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.custom_success_handler', CustomAuthenticationSuccessHandler::class) + ->abstract() + ->args([ + abstract_arg('The custom success handler service'), + [], // Options + abstract_arg('Provider-shared Key'), + ]) + + ->set('security.authentication.success_handler', DefaultAuthenticationSuccessHandler::class) + ->abstract() + ->args([ + service('security.http_utils'), + [], // Options + ]) + + ->set('security.authentication.custom_failure_handler', CustomAuthenticationFailureHandler::class) + ->abstract() + ->args([ + abstract_arg('The custom failure handler service'), + [], // Options + ]) + + ->set('security.authentication.failure_handler', DefaultAuthenticationFailureHandler::class) + ->abstract() + ->args([ + service('http_kernel'), + service('security.http_utils'), + [], // Options + service('logger')->nullOnInvalid(), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.exception_listener', ExceptionListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + service('security.authentication.trust_resolver'), + service('security.http_utils'), + abstract_arg('Provider-shared Key'), + service('security.authentication.entry_point')->nullOnInvalid(), + param('security.access.denied_url'), + service('security.access.denied_handler')->nullOnInvalid(), + service('logger')->nullOnInvalid(), + false, // Stateless + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.authentication.switchuser_listener', SwitchUserListener::class) + ->abstract() + ->args([ + service('security.token_storage'), + abstract_arg('User Provider'), + abstract_arg('User Checker'), + abstract_arg('Provider Key'), + service('security.access.decision_manager'), + service('logger')->nullOnInvalid(), + '_switch_user', + 'ROLE_ALLOWED_TO_SWITCH', + service('event_dispatcher')->nullOnInvalid(), + false, // Stateless + ]) + ->tag('monolog.logger', ['channel' => 'security']) + + ->set('security.access_listener', AccessListener::class) + ->args([ + service('security.token_storage'), + service('security.access.decision_manager'), + service('security.access_map'), + ]) + ->tag('monolog.logger', ['channel' => 'security']) + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml deleted file mode 100644 index 503bd10bed4ed..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ /dev/null @@ -1,252 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - %request_listener.http_port% - %request_listener.https_port% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.2. - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.2. - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.2. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %security.authentication.hide_user_not_found% - - - - - - - - - %security.authentication.hide_user_not_found% - - - - - - - - - null - The "%service_id%" service is deprecated since Symfony 4.2. - - - - - - - - - - - - - - - %security.access.denied_url% - - - false - - - - - - - - - - - _switch_user - ROLE_ALLOWED_TO_SWITCH - - false - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml deleted file mode 100644 index 94aa3a3824ba4..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml deleted file mode 100644 index b2bafbc60546f..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - - - - - The "%service_id%" service is deprecated since Symfony 4.3 and will be removed in 5.0. - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php new file mode 100644 index 0000000000000..05a74d086e820 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.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\DependencyInjection\Loader\Configurator; + +use Symfony\Bridge\Twig\Extension\LogoutUrlExtension; +use Symfony\Bridge\Twig\Extension\SecurityExtension; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('twig.extension.logout_url', LogoutUrlExtension::class) + ->args([ + service('security.logout_url_generator'), + ]) + ->tag('twig.extension') + + ->set('twig.extension.security', SecurityExtension::class) + ->args([ + service('security.authorization_checker')->ignoreOnInvalid(), + service('security.impersonate_url_generator')->ignoreOnInvalid(), + ]) + ->tag('twig.extension') + ; +}; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml deleted file mode 100644 index c07547fa17902..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 59ebea428b7f6..26b59585a1c65 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -3,370 +3,424 @@ {% block page_title 'Security' %} {% block toolbar %} - {% if collector.token %} - {% set is_authenticated = collector.enabled and collector.authenticated %} - {% set color_code = not is_authenticated ? 'yellow' %} - {% else %} - {% set color_code = collector.enabled ? 'red' %} - {% endif %} - - {% set icon %} - {{ include('@Security/Collector/icon.svg') }} - {{ collector.user|default('n/a') }} - {% endset %} - - {% set text %} - {% if collector.impersonated %} -
-
- Impersonator - {{ collector.impersonatorUser }} -
-
- {% endif %} - -
- {% if collector.enabled %} - {% if collector.token %} + {% if collector.firewall %} + {% set icon %} + {{ include('@Security/Collector/icon.svg') }} + {{ collector.user|default('n/a') }} + {% endset %} + + {% set text %} + {% if collector.impersonated %} +
- Logged in as - {{ collector.user }} -
- -
- Authenticated - {{ is_authenticated ? 'Yes' : 'No' }} + Impersonator + {{ collector.impersonatorUser }}
+
+ {% endif %} -
- Token class - {{ collector.tokenClass|abbr_class }} -
- {% else %} -
- Authenticated - No -
- {% endif %} +
+ {% if collector.enabled %} + {% if collector.token %} +
+ Logged in as + {{ collector.user }} +
+ +
+ Authenticated + {{ collector.authenticated ? 'Yes' : 'No' }} +
+ +
+ Roles + + {% set remainingRoles = collector.roles|slice(1) %} + {{ collector.roles|first }} + {% if remainingRoles is not empty %} + + + + {{ remainingRoles|length }} more + + {% endif %} + +
+ +
+ Token class + {{ collector.tokenClass|abbr_class }} +
+ {% else %} +
+ Authenticated + No +
+ {% endif %} - {% if collector.firewall %} -
- Firewall name - {{ collector.firewall.name }} -
- {% endif %} + {% if collector.firewall %} +
+ Firewall name + {{ collector.firewall.name }} +
+ {% endif %} - {% if collector.token and collector.logoutUrl %} + {% if collector.token and collector.logoutUrl %} +
+ Actions + + Logout + {% if collector.impersonated and collector.impersonationExitPath %} + | Exit impersonation + {% endif %} + +
+ {% endif %} + {% else %}
- Actions - - Logout - {% if collector.impersonated and collector.impersonationExitPath %} - | Exit impersonation - {% endif %} - + The security is disabled.
{% endif %} - {% else %} -
- The security is disabled. -
- {% endif %} -
- {% endset %} +
+ {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: color_code }) }} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }} + {% endif %} {% endblock %} {% block menu %} - + {{ include('@Security/Collector/icon.svg') }} Security {% endblock %} {% block panel %} -

Security Token

- +

Security

{% if collector.enabled %} - {% if collector.token %} -
-
- {{ collector.user == 'anon.' ? 'Anonymous' : collector.user }} - Username -
+
+
+

Token

-
- {{ include('@WebProfiler/Icon/' ~ (collector.authenticated ? 'yes' : 'no') ~ '.svg') }} - Authenticated -
-
+
+ {% if collector.token %} +
+
+ {{ collector.user }} + Username +
- - - - - - - - - - - - - - {% if collector.supportsRoleHierarchy %} - - - - - {% endif %} +
+ {{ include('@WebProfiler/Icon/' ~ (collector.authenticated ? 'yes' : 'no') ~ '.svg') }} + Authenticated +
+ + +
PropertyValue
Roles - {{ collector.roles is empty ? 'none' : profiler_dump(collector.roles, maxDepth=1) }} - - {% if not collector.authenticated and collector.roles is empty %} -

User is not authenticated probably because they have no roles.

- {% endif %} -
Inherited Roles{{ collector.inheritedRoles is empty ? 'none' : profiler_dump(collector.inheritedRoles, maxDepth=1) }}
+ + + + + + + + + + + + + {% if collector.supportsRoleHierarchy %} + + + + + {% endif %} - {% if collector.token %} - - - - + {% if collector.token %} + + + + + {% endif %} + +
PropertyValue
Roles + {{ collector.roles is empty ? 'none' : profiler_dump(collector.roles, maxDepth=1) }} + + {% if not collector.authenticated and collector.roles is empty %} +

User is not authenticated probably because they have no roles.

+ {% endif %} +
Inherited Roles{{ collector.inheritedRoles is empty ? 'none' : profiler_dump(collector.inheritedRoles, maxDepth=1) }}
Token{{ profiler_dump(collector.token) }}
Token{{ profiler_dump(collector.token) }}
+ {% elseif collector.enabled %} +
+

There is no security token.

+
{% endif %} - - - {% elseif collector.enabled %} -
-

There is no security token.

+
- {% endif %} - -

Security Firewall

- - {% if collector.firewall %} -
-
- {{ collector.firewall.name }} - Name -
-
- {{ include('@WebProfiler/Icon/' ~ (collector.firewall.security_enabled ? 'yes' : 'no') ~ '.svg') }} - Security enabled -
-
- {{ include('@WebProfiler/Icon/' ~ (collector.firewall.stateless ? 'yes' : 'no') ~ '.svg') }} - Stateless -
-
- {{ include('@WebProfiler/Icon/' ~ (collector.firewall.allows_anonymous ? 'yes' : 'no') ~ '.svg') }} - Allows anonymous +
+

Firewall

+
+ {% if collector.firewall %} +
+
+ {{ collector.firewall.name }} + Name +
+
+ {{ include('@WebProfiler/Icon/' ~ (collector.firewall.security_enabled ? 'yes' : 'no') ~ '.svg') }} + Security enabled +
+
+ {{ include('@WebProfiler/Icon/' ~ (collector.firewall.stateless ? 'yes' : 'no') ~ '.svg') }} + Stateless +
+
+ + {% if collector.firewall.security_enabled %} +

Configuration

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyValue
provider{{ collector.firewall.provider ?: '(none)' }}
context{{ collector.firewall.context ?: '(none)' }}
entry_point{{ collector.firewall.entry_point ?: '(none)' }}
user_checker{{ collector.firewall.user_checker ?: '(none)' }}
access_denied_handler{{ collector.firewall.access_denied_handler ?: '(none)' }}
access_denied_url{{ collector.firewall.access_denied_url ?: '(none)' }}
authenticators{{ collector.firewall.authenticators is empty ? '(none)' : profiler_dump(collector.firewall.authenticators, maxDepth=1) }}
+ {% endif %} + {% endif %}
- {% if collector.firewall.security_enabled %} -

Configuration

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyValue
provider{{ collector.firewall.provider ?: '(none)' }}
context{{ collector.firewall.context ?: '(none)' }}
entry_point{{ collector.firewall.entry_point ?: '(none)' }}
user_checker{{ collector.firewall.user_checker ?: '(none)' }}
access_denied_handler{{ collector.firewall.access_denied_handler ?: '(none)' }}
access_denied_url{{ collector.firewall.access_denied_url ?: '(none)' }}
listeners{{ collector.firewall.listeners is empty ? '(none)' : profiler_dump(collector.firewall.listeners, maxDepth=1) }}
- -

Listeners

- - {% if collector.listeners|default([]) is empty %} -
-

No security listeners have been recorded. Check that debugging is enabled in the kernel.

-
- {% else %} - - - - - - - - - - {% set previous_event = (collector.listeners|first) %} - {% for listener in collector.listeners %} - {% if loop.first or listener != previous_event %} - {% if not loop.first %} - +
+

Listeners

+
+ {% if collector.listeners|default([]) is empty %} +
+

No security listeners have been recorded. Check that debugging is enabled in the kernel.

+
+ {% else %} +
ListenerDurationResponse
+ + + + + + + + + {% set previous_event = (collector.listeners|first) %} + {% for listener in collector.listeners %} + {% if loop.first or listener != previous_event %} + {% if not loop.first %} + + {% endif %} + + {% set previous_event = listener %} {% endif %} - - {% set previous_event = listener %} - {% endif %} + + + + + + + {% if loop.last %} + + {% endif %} + {% endfor %} +
ListenerDurationResponse
{{ profiler_dump(listener.stub) }}{{ '%0.2f'|format(listener.time * 1000) }} ms{{ listener.response ? profiler_dump(listener.response) : '(none)' }}
+ {% endif %} +
+
+
+

Authenticators

+
+ {% if collector.authenticators|default([]) is not empty %} + + - - - + + + + + - {% if loop.last %} - - {% endif %} - {% endfor %} -
{{ profiler_dump(listener.stub) }}{{ '%0.2f'|format(listener.time * 1000) }} ms{{ listener.response ? profiler_dump(listener.response) : '(none)' }}AuthenticatorSupportsDurationPassport
- {% endif %} - {% endif %} - {% elseif collector.enabled %} -
-

This request was not covered by any firewall.

-
- {% endif %} - {% else %} -
-

The security component is disabled.

-
- {% endif %} - - {% if collector.voters|default([]) is not empty %} -

Security Voters ({{ collector.voters|length }})

+ {% set previous_event = (collector.listeners|first) %} + {% for authenticator in collector.authenticators %} + {% if loop.first or authenticator != previous_event %} + {% if not loop.first %} + + {% endif %} -
-
- {{ collector.voterStrategy|default('unknown') }} - Strategy -
-
+ + {% set previous_event = authenticator %} + {% endif %} - - - - - - - - - - {% for voter in collector.voters %} - - - - - {% endfor %} - -
#Voter class
{{ loop.index }}{{ profiler_dump(voter) }}
- {% endif %} + + {{ profiler_dump(authenticator.stub) }} + {{ include('@WebProfiler/Icon/' ~ (authenticator.supports ? 'yes' : 'no') ~ '.svg') }} + {{ '%0.2f'|format(authenticator.duration * 1000) }} ms + {{ authenticator.passport ? profiler_dump(authenticator.passport) : '(none)' }} + - {% if collector.accessDecisionLog|default([]) is not empty %} -

Access decision log

- - - - - - - - - - - - - - - - - - {% for decision in collector.accessDecisionLog %} - - - - {% endif %} - {% else %} - {{ profiler_dump(decision.attributes) }} - {% endif %} - - - - - - - - {% endfor %} - -
#ResultAttributesObject
{{ loop.index }} - {{ decision.result - ? 'GRANTED' - : 'DENIED' - }} - - {% if decision.attributes|length == 1 %} - {% set attribute = decision.attributes|first %} - {% if attribute.expression is defined %} - Expression:
{{ attribute.expression }}
- {% elseif attribute.type == 'string' %} - {{ attribute }} - {% else %} - {{ profiler_dump(attribute) }} + {% if loop.last %} +
{{ profiler_dump(decision.seek('object')) }}
- {% if decision.voter_details is not empty %} - {% set voter_details_id = 'voter-details-' ~ loop.index %} -
- - - {% for voter_detail in decision.voter_details %} - - - {% if collector.voterStrategy == constant('Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager::STRATEGY_UNANIMOUS') %} - - {% endif %} -
{{ profiler_dump(voter_detail['class']) }}attribute {{ voter_detail['attributes'][0] }} - {% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %} - ACCESS GRANTED - {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %} - ACCESS ABSTAIN - {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %} - ACCESS DENIED + {% endfor %} +
+ {% else %} +
+

No authenticators have been recorded. Check previous profiles on your authentication endpoint.

+
+ {% endif %} +
+ + +
+

Access Decision

+
+ {% if collector.voters|default([]) is not empty %} +
+
+ {{ collector.voterStrategy|default('unknown') }} + Strategy +
+
+ + + + + + + + + + + {% for voter in collector.voters %} + + + + + {% endfor %} + +
#Voter class
{{ loop.index }}{{ profiler_dump(voter) }}
+ {% endif %} + {% if collector.accessDecisionLog|default([]) is not empty %} +

Access decision log

+ + + + + + + + + + + + + + + + + + {% for decision in collector.accessDecisionLog %} + + + + - - {% endfor %} - -
#ResultAttributesObject
{{ loop.index }} + {{ decision.result + ? 'GRANTED' + : 'DENIED' + }} + + {% if decision.attributes|length == 1 %} + {% set attribute = decision.attributes|first %} + {% if attribute.expression is defined %} + Expression:
{{ attribute.expression }}
+ {% elseif attribute.type == 'string' %} + {{ attribute }} {% else %} - unknown ({{ voter_detail['vote'] }}) + {{ profiler_dump(attribute) }} {% endif %} -
-
- Show voter details - {% endif %} -
+ {% else %} + {{ profiler_dump(decision.attributes) }} + {% endif %} + + {{ profiler_dump(decision.seek('object')) }} + + + + + {% if decision.voter_details is not empty %} + {% set voter_details_id = 'voter-details-' ~ loop.index %} +
+ + + {% for voter_detail in decision.voter_details %} + + + {% if collector.voterStrategy == 'unanimous' %} + + {% endif %} + + + {% endfor %} + +
{{ profiler_dump(voter_detail['class']) }}attribute {{ voter_detail['attributes'][0] }} + {% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %} + ACCESS GRANTED + {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %} + ACCESS ABSTAIN + {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %} + ACCESS DENIED + {% else %} + unknown ({{ voter_detail['vote'] }}) + {% endif %} +
+
+ Show voter details + {% endif %} + + + {% endfor %} + + +
+ {% endif %} +
+
{% endif %} {% endblock %} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php new file mode 100644 index 0000000000000..d79d0b7a1df53 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallAwareTrait.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +/** + * Provides basic functionality for services mapped by the firewall name + * in a container locator. + * + * @author Wouter de Jong + * + * @internal + */ +trait FirewallAwareTrait +{ + private $locator; + private $requestStack; + private $firewallMap; + + private function getForFirewall(): object + { + $serviceIdentifier = str_replace('FirewallAware', '', static::class); + if (null === $request = $this->requestStack->getCurrentRequest()) { + throw new \LogicException('Cannot determine the correct '.$serviceIdentifier.' to use: there is no active Request and so, the firewall cannot be determined. Try using a specific '.$serviceIdentifier.' service.'); + } + + $firewall = $this->firewallMap->getFirewallConfig($request); + if (!$firewall) { + throw new \LogicException('No '.$serviceIdentifier.' found as the current route is not covered by a firewall.'); + } + + $firewallName = $firewall->getName(); + if (!$this->locator->has($firewallName)) { + $message = 'No '.$serviceIdentifier.' found for this firewall.'; + if (\defined(static::class.'::FIREWALL_OPTION')) { + $message .= sprintf('Did you forget to add a "'.static::FIREWALL_OPTION.'" key under your "%s" firewall?', $firewallName); + } + + throw new \LogicException($message); + } + + return $this->locator->get($firewallName); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php index dca8ccde565c1..ce9373f91db72 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php @@ -16,20 +16,20 @@ */ final class FirewallConfig { - private $name; - private $userChecker; - private $requestMatcher; - private $securityEnabled; - private $stateless; - private $provider; - private $context; - private $entryPoint; - private $accessDeniedHandler; - private $accessDeniedUrl; - private $listeners; - private $switchUser; - - public function __construct(string $name, string $userChecker, string $requestMatcher = null, bool $securityEnabled = true, bool $stateless = false, string $provider = null, string $context = null, string $entryPoint = null, string $accessDeniedHandler = null, string $accessDeniedUrl = null, array $listeners = [], $switchUser = null) + private string $name; + private string $userChecker; + private ?string $requestMatcher; + private bool $securityEnabled; + private bool $stateless; + private ?string $provider; + private ?string $context; + private ?string $entryPoint; + private ?string $accessDeniedHandler; + private ?string $accessDeniedUrl; + private array $authenticators; + private ?array $switchUser; + + public function __construct(string $name, string $userChecker, string $requestMatcher = null, bool $securityEnabled = true, bool $stateless = false, string $provider = null, string $context = null, string $entryPoint = null, string $accessDeniedHandler = null, string $accessDeniedUrl = null, array $authenticators = [], array $switchUser = null) { $this->name = $name; $this->userChecker = $userChecker; @@ -41,7 +41,7 @@ public function __construct(string $name, string $userChecker, string $requestMa $this->entryPoint = $entryPoint; $this->accessDeniedHandler = $accessDeniedHandler; $this->accessDeniedUrl = $accessDeniedUrl; - $this->listeners = $listeners; + $this->authenticators = $authenticators; $this->switchUser = $switchUser; } @@ -64,11 +64,6 @@ public function isSecurityEnabled(): bool return $this->securityEnabled; } - public function allowsAnonymous(): bool - { - return \in_array('anonymous', $this->listeners, true); - } - public function isStateless(): bool { return $this->stateless; @@ -107,9 +102,9 @@ public function getAccessDeniedUrl(): ?string return $this->accessDeniedUrl; } - public function getListeners(): array + public function getAuthenticators(): array { - return $this->listeners; + return $this->authenticators; } public function getSwitchUser(): ?array diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php index 25cc078af2da2..3813420d28081 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php @@ -22,27 +22,20 @@ */ class FirewallContext { - private $listeners; + private iterable $listeners; private $exceptionListener; private $logoutListener; private $config; /** - * @param LogoutListener|null $logoutListener + * @param iterable $listeners */ - public function __construct(iterable $listeners, ExceptionListener $exceptionListener = null, $logoutListener = null, FirewallConfig $config = null) + public function __construct(iterable $listeners, ExceptionListener $exceptionListener = null, LogoutListener $logoutListener = null, FirewallConfig $config = null) { $this->listeners = $listeners; $this->exceptionListener = $exceptionListener; - if ($logoutListener instanceof FirewallConfig) { - $this->config = $logoutListener; - @trigger_error(sprintf('Passing an instance of %s as the 3rd argument to "%s()" is deprecated since Symfony 4.2. Pass a %s instance instead.', FirewallConfig::class, __METHOD__, LogoutListener::class), \E_USER_DEPRECATED); - } elseif (null === $logoutListener || $logoutListener instanceof LogoutListener) { - $this->logoutListener = $logoutListener; - $this->config = $config; - } else { - throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be instance of "%s" or null, "%s" given.', __METHOD__, LogoutListener::class, \is_object($logoutListener) ? \get_class($logoutListener) : \gettype($logoutListener))); - } + $this->logoutListener = $logoutListener; + $this->config = $config; } public function getConfig() @@ -50,6 +43,9 @@ public function getConfig() return $this->config; } + /** + * @return iterable + */ public function getListeners(): iterable { return $this->listeners; diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php index 06cbc64d18c6b..4c8543df73fda 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php @@ -25,7 +25,7 @@ class FirewallMap implements FirewallMapInterface { private $container; - private $map; + private iterable $map; public function __construct(ContainerInterface $container, iterable $map) { @@ -33,7 +33,7 @@ public function __construct(ContainerInterface $container, iterable $map) $this->map = $map; } - public function getListeners(Request $request) + public function getListeners(Request $request): array { $context = $this->getFirewallContext($request); @@ -44,10 +44,7 @@ public function getListeners(Request $request) return [$context->getListeners(), $context->getExceptionListener(), $context->getLogoutListener()]; } - /** - * @return FirewallConfig|null - */ - public function getFirewallConfig(Request $request) + public function getFirewallConfig(Request $request): ?FirewallConfig { $context = $this->getFirewallContext($request); diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php index 65144b77494ac..9d8396a8830c9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php @@ -14,8 +14,8 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Http\Event\LazyResponseEvent; -use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; use Symfony\Component\Security\Http\Firewall\LogoutListener; /** @@ -46,13 +46,9 @@ public function __invoke(RequestEvent $event) $lazy = $request->isMethodCacheable(); foreach (parent::getListeners() as $listener) { - if (!\is_callable($listener)) { - @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($listener), AbstractListener::class), \E_USER_DEPRECATED); - $listeners[] = [$listener, 'handle']; - $lazy = false; - } elseif (!$lazy || !$listener instanceof AbstractListener) { + if (!$lazy || !$listener instanceof FirewallListenerInterface) { $listeners[] = $listener; - $lazy = $lazy && $listener instanceof AbstractListener; + $lazy = $lazy && $listener instanceof FirewallListenerInterface; } elseif (false !== $supports = $listener->supports($request)) { $listeners[] = [$listener, 'authenticate']; $lazy = null === $supports; diff --git a/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php new file mode 100644 index 0000000000000..174f1d015e729 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; + +/** + * A decorator that delegates all method calls to the authenticator + * manager of the current firewall. + * + * @author Wouter de Jong + * + * @final + */ +class UserAuthenticator implements UserAuthenticatorInterface +{ + use FirewallAwareTrait; + + public function __construct(FirewallMap $firewallMap, ContainerInterface $userAuthenticators, RequestStack $requestStack) + { + $this->firewallMap = $firewallMap; + $this->locator = $userAuthenticators; + $this->requestStack = $requestStack; + } + + /** + * {@inheritdoc} + */ + public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response + { + return $this->getForFirewall()->authenticateUser($user, $authenticator, $request, $badges); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 24cf5569b819b..01eeb74c3a094 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -14,32 +14,34 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLanguageProvidersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\CleanRememberMeVerifierPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPointPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\JsonLoginLdapFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\LoginLinkFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\LoginThrottlingFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RemoteUserFactory; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePreAuthenticationFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\LdapFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\Security\Core\AuthenticationEvents; -use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; -use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; -use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; -use Symfony\Component\Security\Http\Event\SwitchUserEvent; use Symfony\Component\Security\Http\SecurityEvents; /** @@ -53,34 +55,40 @@ public function build(ContainerBuilder $container) { parent::build($container); + /** @var SecurityExtension $extension */ $extension = $container->getExtension('security'); - $extension->addSecurityListenerFactory(new FormLoginFactory()); - $extension->addSecurityListenerFactory(new FormLoginLdapFactory()); - $extension->addSecurityListenerFactory(new JsonLoginFactory()); - $extension->addSecurityListenerFactory(new JsonLoginLdapFactory()); - $extension->addSecurityListenerFactory(new HttpBasicFactory()); - $extension->addSecurityListenerFactory(new HttpBasicLdapFactory()); - $extension->addSecurityListenerFactory(new RememberMeFactory()); - $extension->addSecurityListenerFactory(new X509Factory()); - $extension->addSecurityListenerFactory(new RemoteUserFactory()); - $extension->addSecurityListenerFactory(new SimplePreAuthenticationFactory(false)); - $extension->addSecurityListenerFactory(new SimpleFormFactory(false)); - $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); - $extension->addSecurityListenerFactory(new AnonymousFactory()); + $extension->addAuthenticatorFactory(new FormLoginFactory()); + $extension->addAuthenticatorFactory(new FormLoginLdapFactory()); + $extension->addAuthenticatorFactory(new JsonLoginFactory()); + $extension->addAuthenticatorFactory(new JsonLoginLdapFactory()); + $extension->addAuthenticatorFactory(new HttpBasicFactory()); + $extension->addAuthenticatorFactory(new HttpBasicLdapFactory()); + $extension->addAuthenticatorFactory(new RememberMeFactory()); + $extension->addAuthenticatorFactory(new X509Factory()); + $extension->addAuthenticatorFactory(new RemoteUserFactory()); + $extension->addAuthenticatorFactory(new CustomAuthenticatorFactory()); + $extension->addAuthenticatorFactory(new LoginThrottlingFactory()); + $extension->addAuthenticatorFactory(new LoginLinkFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); $container->addCompilerPass(new AddExpressionLanguageProvidersPass()); $container->addCompilerPass(new AddSecurityVotersPass()); $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING); - $container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass()); + $container->addCompilerPass(new CleanRememberMeVerifierPass()); + $container->addCompilerPass(new RegisterCsrfFeaturesPass()); $container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); + $container->addCompilerPass(new RegisterLdapLocatorPass()); + $container->addCompilerPass(new RegisterEntryPointPass()); + // must be registered after RegisterListenersPass (in the FrameworkBundle) + $container->addCompilerPass(new RegisterGlobalSecurityEventListenersPass(), PassConfig::TYPE_BEFORE_REMOVING, -200); + // execute after ResolveChildDefinitionsPass optimization pass, to ensure class names are set + $container->addCompilerPass(new SortFirewallListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass(new ReplaceDecoratedRememberMeHandlerPass(), PassConfig::TYPE_OPTIMIZE); - $container->addCompilerPass(new AddEventAliasesPass([ - AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS, - AuthenticationFailureEvent::class => AuthenticationEvents::AUTHENTICATION_FAILURE, - InteractiveLoginEvent::class => SecurityEvents::INTERACTIVE_LOGIN, - SwitchUserEvent::class => SecurityEvents::SWITCH_USER, - ])); + $container->addCompilerPass(new AddEventAliasesPass(array_merge( + AuthenticationEvents::ALIASES, + SecurityEvents::ALIASES + ))); } } diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php b/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php deleted file mode 100644 index 939b2f2dfcbe6..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Http\Controller\UserValueResolver; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1, use "%s" instead.', SecurityUserValueResolver::class, UserValueResolver::class), \E_USER_DEPRECATED); - -/** - * Supports the argument type of {@see UserInterface}. - * - * @author Iltar van der Berg - * - * @deprecated since Symfony 4.1, use {@link UserValueResolver} instead - */ -final class SecurityUserValueResolver implements ArgumentValueResolverInterface -{ - private $tokenStorage; - - public function __construct(TokenStorageInterface $tokenStorage) - { - $this->tokenStorage = $tokenStorage; - } - - public function supports(Request $request, ArgumentMetadata $argument): bool - { - // only security user implementations are supported - if (UserInterface::class !== $argument->getType()) { - return false; - } - - $token = $this->tokenStorage->getToken(); - if (!$token instanceof TokenInterface) { - return false; - } - - $user = $token->getUser(); - - // in case it's not an object we cannot do anything with it; E.g. "anon." - return $user instanceof UserInterface; - } - - public function resolve(Request $request, ArgumentMetadata $argument): iterable - { - yield $this->tokenStorage->getToken()->getUser(); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php deleted file mode 100644 index 0e8e518fee27a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Templating\Helper; - -@trigger_error('The '.LogoutUrlHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; -use Symfony\Component\Templating\Helper\Helper; - -/** - * LogoutUrlHelper provides generator functions for the logout URL. - * - * @author Jeremy Mikola - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class LogoutUrlHelper extends Helper -{ - private $generator; - - public function __construct(LogoutUrlGenerator $generator) - { - $this->generator = $generator; - } - - /** - * Generates the absolute logout path for the firewall. - * - * @param string|null $key The firewall key or null to use the current firewall key - * - * @return string The logout path - */ - public function getLogoutPath($key) - { - return $this->generator->getLogoutPath($key); - } - - /** - * Generates the absolute logout URL for the firewall. - * - * @param string|null $key The firewall key or null to use the current firewall key - * - * @return string The logout URL - */ - public function getLogoutUrl($key) - { - return $this->generator->getLogoutUrl($key); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'logout_url'; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php deleted file mode 100644 index 5a996d64e63a0..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Templating\Helper; - -@trigger_error('The '.SecurityHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Security\Acl\Voter\FieldVote; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Symfony\Component\Templating\Helper\Helper; - -/** - * SecurityHelper provides read-only access to the security checker. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig instead. - */ -class SecurityHelper extends Helper -{ - private $securityChecker; - - public function __construct(AuthorizationCheckerInterface $securityChecker = null) - { - $this->securityChecker = $securityChecker; - } - - public function isGranted($role, $object = null, $field = null) - { - if (null === $this->securityChecker) { - return false; - } - - if (null !== $field) { - $object = new FieldVote($object, $field); - } - - return $this->securityChecker->isGranted($role, $object); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'security'; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php index 9143db38edffd..53b16fcdf7774 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php @@ -26,8 +26,8 @@ public function testWarmUp() $expressionLang->expects($this->exactly(2)) ->method('parse') ->withConsecutive( - [$expressions[0], ['token', 'user', 'object', 'subject', 'roles', 'role_names', 'request', 'trust_resolver']], - [$expressions[1], ['token', 'user', 'object', 'subject', 'roles', 'role_names', 'request', 'trust_resolver']] + [$expressions[0], ['token', 'user', 'object', 'subject', 'role_names', 'request', 'trust_resolver']], + [$expressions[1], ['token', 'user', 'object', 'subject', 'role_names', 'request', 'trust_resolver']] ); (new ExpressionCacheWarmer($expressions, $expressionLang))->warmUp(''); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 74bb0404a8e8a..c3283dae5cdd7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector; use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; +use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration; use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -24,13 +25,11 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; -use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\RoleHierarchy; -use Symfony\Component\Security\Core\Role\SwitchUserRole; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -39,7 +38,7 @@ class SecurityDataCollectorTest extends TestCase { public function testCollectWhenSecurityIsDisabled() { - $collector = new SecurityDataCollector(); + $collector = new SecurityDataCollector(null, null, null, null, null, null, true); $collector->collect(new Request(), new Response()); $this->assertSame('security', $collector->getName()); @@ -59,7 +58,7 @@ public function testCollectWhenSecurityIsDisabled() public function testCollectWhenAuthenticationTokenIsNull() { $tokenStorage = new TokenStorage(); - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true); $collector->collect(new Request(), new Response()); $this->assertTrue($collector->isEnabled()); @@ -79,9 +78,9 @@ public function testCollectWhenAuthenticationTokenIsNull() public function testCollectAuthenticationTokenAndRoles(array $roles, array $normalizedRoles, array $inheritedRoles) { $tokenStorage = new TokenStorage(); - $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles)); + $tokenStorage->setToken(new UsernamePasswordToken(new InMemoryUser('hhamon', 'P4$$w0rD', $roles), 'provider', $roles)); - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true); $collector->collect(new Request(), new Response()); $collector->lateCollect(); @@ -97,44 +96,14 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm $this->assertSame('hhamon', $collector->getUser()); } - /** - * @group legacy - */ - public function testCollectImpersonatedToken() - { - $adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', ['ROLE_ADMIN']); - - $userRoles = [ - 'ROLE_USER', - new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $adminToken), - ]; - - $tokenStorage = new TokenStorage(); - $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles)); - - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); - $collector->collect(new Request(), new Response()); - $collector->lateCollect(); - - $this->assertTrue($collector->isEnabled()); - $this->assertTrue($collector->isAuthenticated()); - $this->assertTrue($collector->isImpersonated()); - $this->assertSame('yceruto', $collector->getImpersonatorUser()); - $this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue()); - $this->assertTrue($collector->supportsRoleHierarchy()); - $this->assertSame(['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $collector->getRoles()->getValue(true)); - $this->assertSame([], $collector->getInheritedRoles()->getValue(true)); - $this->assertSame('hhamon', $collector->getUser()); - } - public function testCollectSwitchUserToken() { - $adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', ['ROLE_ADMIN']); + $adminToken = new UsernamePasswordToken(new InMemoryUser('yceruto', 'P4$$w0rD', ['ROLE_ADMIN']), 'provider', ['ROLE_ADMIN']); $tokenStorage = new TokenStorage(); - $tokenStorage->setToken(new SwitchUserToken('hhamon', 'P4$$w0rD', 'provider', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $adminToken)); + $tokenStorage->setToken(new SwitchUserToken(new InMemoryUser('hhamon', 'P4$$w0rD', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN']), 'provider', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $adminToken)); - $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true); $collector->collect(new Request(), new Response()); $collector->lateCollect(); @@ -164,13 +133,12 @@ public function testGetFirewall() ->with($request) ->willReturn($firewallConfig); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true); $collector->collect($request, new Response()); $collector->lateCollect(); $collected = $collector->getFirewall(); $this->assertSame($firewallConfig->getName(), $collected['name']); - $this->assertSame($firewallConfig->allowsAnonymous(), $collected['allows_anonymous']); $this->assertSame($firewallConfig->getRequestMatcher(), $collected['request_matcher']); $this->assertSame($firewallConfig->isSecurityEnabled(), $collected['security_enabled']); $this->assertSame($firewallConfig->isStateless(), $collected['stateless']); @@ -180,7 +148,7 @@ public function testGetFirewall() $this->assertSame($firewallConfig->getAccessDeniedHandler(), $collected['access_denied_handler']); $this->assertSame($firewallConfig->getAccessDeniedUrl(), $collected['access_denied_url']); $this->assertSame($firewallConfig->getUserChecker(), $collected['user_checker']); - $this->assertSame($firewallConfig->getListeners(), $collected['listeners']->getValue()); + $this->assertSame($firewallConfig->getAuthenticators(), $collected['authenticators']->getValue()); } public function testGetFirewallReturnsNull() @@ -189,7 +157,7 @@ public function testGetFirewallReturnsNull() $response = new Response(); // Don't inject any firewall map - $collector = new SecurityDataCollector(); + $collector = new SecurityDataCollector(null, null, null, null, null, null, true); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); @@ -199,7 +167,7 @@ public function testGetFirewallReturnsNull() ->disableOriginalConstructor() ->getMock(); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); @@ -209,7 +177,7 @@ public function testGetFirewallReturnsNull() ->disableOriginalConstructor() ->getMock(); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true); $collector->collect($request, $response); $this->assertNull($collector->getFirewall()); } @@ -220,7 +188,7 @@ public function testGetFirewallReturnsNull() public function testGetListeners() { $request = new Request(); - $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $event->setResponse($response = new Response()); $listener = function ($e) use ($event, &$listenerCalled) { $listenerCalled += $e === $event; @@ -243,7 +211,7 @@ public function testGetListeners() $firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()); $firewall->onKernelRequest($event); - $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall); + $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall, true); $collector->collect($request, $response); $this->assertNotEmpty($collected = $collector->getListeners()[0]); @@ -260,7 +228,7 @@ public function providerCollectDecisionLog(): \Generator $decoratedVoter1 = new TraceableVoter($voter1, $eventDispatcher); yield [ - AccessDecisionManager::STRATEGY_AFFIRMATIVE, + MainConfiguration::STRATEGY_AFFIRMATIVE, [[ 'attributes' => ['view'], 'object' => new \stdClass(), @@ -284,7 +252,7 @@ public function providerCollectDecisionLog(): \Generator ]; yield [ - AccessDecisionManager::STRATEGY_UNANIMOUS, + MainConfiguration::STRATEGY_UNANIMOUS, [ [ 'attributes' => ['view', 'edit'], @@ -368,7 +336,7 @@ public function testCollectDecisionLog(string $strategy, array $decisionLog, arr ->method('getDecisionLog') ->willReturn($decisionLog); - $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager); + $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null, true); $dataCollector->collect(new Request(), new Response()); $this->assertEquals($dataCollector->getAccessDecisionLog(), $expectedDecisionLog, 'Wrong value returned by getAccessDecisionLog'); @@ -390,22 +358,12 @@ public function provideRoles() ['ROLE_USER'], [], ], - [ - [new Role('ROLE_USER', false)], - ['ROLE_USER'], - [], - ], // Inherited roles [ ['ROLE_ADMIN'], ['ROLE_ADMIN'], ['ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'], ], - [ - [new Role('ROLE_ADMIN', false)], - ['ROLE_ADMIN'], - ['ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'], - ], [ ['ROLE_ADMIN', 'ROLE_OPERATOR'], ['ROLE_ADMIN', 'ROLE_OPERATOR'], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php index c1be247e812f7..6dad1f3a72913 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php @@ -19,6 +19,15 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener; +use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; /** @@ -29,7 +38,7 @@ class TraceableFirewallListenerTest extends TestCase public function testOnKernelRequestRecordsListeners() { $request = new Request(); - $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $event->setResponse($response = new Response()); $listener = function ($e) use ($event, &$listenerCalled) { $listenerCalled += $e === $event; @@ -54,4 +63,78 @@ public function testOnKernelRequestRecordsListeners() $this->assertCount(1, $listeners); $this->assertSame($listener, $listeners[0]['stub']); } + + public function testOnKernelRequestRecordsAuthenticatorsInfo() + { + $request = new Request(); + + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + $event->setResponse($response = new Response()); + + $supportingAuthenticator = $this->createMock(DummyAuthenticator::class); + $supportingAuthenticator + ->method('supports') + ->with($request) + ->willReturn(true); + $supportingAuthenticator + ->expects($this->once()) + ->method('authenticate') + ->with($request) + ->willReturn(new SelfValidatingPassport(new UserBadge('robin', function () {}))); + $supportingAuthenticator + ->expects($this->once()) + ->method('onAuthenticationSuccess') + ->willReturn($response); + $supportingAuthenticator + ->expects($this->once()) + ->method('createToken') + ->willReturn($this->createMock(TokenInterface::class)); + + $notSupportingAuthenticator = $this->createMock(DummyAuthenticator::class); + $notSupportingAuthenticator + ->method('supports') + ->with($request) + ->willReturn(false); + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $dispatcher = new EventDispatcher(); + $authenticatorManager = new AuthenticatorManager( + [$notSupportingAuthenticator, $supportingAuthenticator], + $tokenStorage, + $dispatcher, + 'main' + ); + + $listener = new TraceableAuthenticatorManagerListener(new AuthenticatorManagerListener($authenticatorManager)); + $firewallMap = $this->createMock(FirewallMap::class); + $firewallMap + ->expects($this->once()) + ->method('getFirewallConfig') + ->with($request) + ->willReturn(null); + $firewallMap + ->expects($this->once()) + ->method('getListeners') + ->with($request) + ->willReturn([[$listener], null, null]); + + $firewall = new TraceableFirewallListener($firewallMap, $dispatcher, new LogoutUrlGenerator()); + $firewall->configureLogoutUrlGenerator($event); + $firewall->onKernelRequest($event); + + $this->assertCount(2, $authenticatorsInfo = $firewall->getAuthenticatorsInfo()); + + $this->assertFalse($authenticatorsInfo[0]['supports']); + $this->assertStringContainsString('DummyAuthenticator', $authenticatorsInfo[0]['stub']); + + $this->assertTrue($authenticatorsInfo[1]['supports']); + $this->assertStringContainsString('DummyAuthenticator', $authenticatorsInfo[1]['stub']); + } +} + +abstract class DummyAuthenticator implements InteractiveAuthenticatorInterface +{ + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php index 7dcf7526bd552..e9dfde9344fcd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSessionDomainConstraintPassTest.php @@ -12,10 +12,14 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\HttpFoundation\Request; class AddSessionDomainConstraintPassTest extends TestCase @@ -121,11 +125,11 @@ private function createContainer($sessionStorageOptions) $container = new ContainerBuilder(); $container->setParameter('kernel.bundles_metadata', []); $container->setParameter('kernel.cache_dir', __DIR__); + $container->setParameter('kernel.build_dir', __DIR__); $container->setParameter('kernel.charset', 'UTF-8'); $container->setParameter('kernel.container_class', 'cc'); $container->setParameter('kernel.debug', true); $container->setParameter('kernel.project_dir', __DIR__); - $container->setParameter('kernel.root_dir', __DIR__); $container->setParameter('kernel.secret', __DIR__); if (null !== $sessionStorageOptions) { $container->setParameter('session.storage.options', $sessionStorageOptions); @@ -135,13 +139,14 @@ private function createContainer($sessionStorageOptions) $config = [ 'security' => [ + 'enable_authenticator_manager' => true, 'providers' => ['some_provider' => ['id' => 'foo']], 'firewalls' => ['some_firewall' => ['security' => false]], ], ]; $ext = new FrameworkExtension(); - $ext->load(['framework' => ['csrf_protection' => false, 'router' => ['resource' => 'dummy']]], $container); + $ext->load(['framework' => ['csrf_protection' => false, 'router' => ['resource' => 'dummy', 'utf8' => true]]], $container); $ext = new SecurityExtension(); $ext->load($config, $container); @@ -149,6 +154,9 @@ private function createContainer($sessionStorageOptions) $pass = new AddSessionDomainConstraintPass(); $pass->process($container); + $container->setDefinition('.service_subscriber.fallback_container', new Definition(Container::class)); + $container->setAlias(ContainerInterface::class, new Alias('.service_subscriber.fallback_container', false)); + return $container; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php new file mode 100644 index 0000000000000..b10b8a810bc7a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterEntryPointsPassTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPointPass; +use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; + +class RegisterEntryPointsPassTest extends TestCase +{ + public function testProcessResolvesChildDefinitionsClass() + { + $container = new ContainerBuilder(); + + $container->setParameter('security.firewalls', ['main']); + $container->setParameter('security.main._indexed_authenticators', ['custom' => 'security.authenticator.custom_authenticator.main']); + + $container->register('security.authenticator.manager.main', AuthenticatorManager::class); + $container->register('security.exception_listener.main', ExceptionListener::class)->setArguments([ + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + null, // entry point + ]); + $config = $container->register('security.firewall.map.config.main', FirewallConfig::class); + $config->setArguments([ + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + new AbstractArgument(), + null, // entry point, + ]); + + $container->register('custom_authenticator', CustomAuthenticator::class) + ->setAbstract(true); + + $container->setDefinition('security.authenticator.custom_authenticator.main', new ChildDefinition('custom_authenticator')); + + (new RegisterEntryPointPass())->process($container); + + $this->assertSame('security.authenticator.custom_authenticator.main', $config->getArgument(7)); + } +} + +class CustomAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface +{ + public function supports(Request $request): ?bool + { + return false; + } + + public function authenticate(Request $request): Passport + { + throw new BadCredentialsException(); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse([ + 'error' => $exception->getMessageKey(), + ], JsonResponse::HTTP_FORBIDDEN); + } + + public function start(Request $request, AuthenticationException $authException = null): Response + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPassTest.php new file mode 100644 index 0000000000000..bd0ea68520abe --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterGlobalSecurityEventListenersPassTest.php @@ -0,0 +1,219 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\LoginFailureEvent; +use Symfony\Component\Security\Http\Event\LoginSuccessEvent; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\SecurityEvents; + +class RegisterGlobalSecurityEventListenersPassTest extends TestCase +{ + private $container; + + protected function setUp(): void + { + $this->container = new ContainerBuilder(); + $this->container->setParameter('kernel.debug', false); + $this->container->register('request_stack', \stdClass::class); + $this->container->register('event_dispatcher', EventDispatcher::class); + + $this->container->registerExtension(new SecurityExtension()); + + $this->container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); + $this->container->getCompilerPassConfig()->setRemovingPasses([]); + $this->container->getCompilerPassConfig()->setAfterRemovingPasses([]); + + $securityBundle = new SecurityBundle(); + $securityBundle->build($this->container); + } + + /** + * @dataProvider providePropagatedEvents + */ + public function testEventIsPropagated(string $configuredEvent, string $registeredEvent) + { + $this->container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]], + ]); + + $this->container->register('app.security_listener', \stdClass::class) + ->addTag('kernel.event_listener', ['method' => 'onEvent', 'event' => $configuredEvent]); + + $this->container->compile(); + + $this->assertListeners([ + [$registeredEvent, ['app.security_listener', 'onEvent'], 0], + ]); + } + + public function providePropagatedEvents(): array + { + return [ + [CheckPassportEvent::class, CheckPassportEvent::class], + [LoginFailureEvent::class, LoginFailureEvent::class], + [LoginSuccessEvent::class, LoginSuccessEvent::class], + [LogoutEvent::class, LogoutEvent::class], + [AuthenticationTokenCreatedEvent::class, AuthenticationTokenCreatedEvent::class], + [AuthenticationEvents::AUTHENTICATION_SUCCESS, AuthenticationEvents::AUTHENTICATION_SUCCESS], + [SecurityEvents::INTERACTIVE_LOGIN, SecurityEvents::INTERACTIVE_LOGIN], + + // These events are ultimately registered by their event name instead of the FQN + [AuthenticationSuccessEvent::class, AuthenticationEvents::AUTHENTICATION_SUCCESS], + [InteractiveLoginEvent::class, SecurityEvents::INTERACTIVE_LOGIN], + ]; + } + + public function testRegisterCustomListener() + { + $this->container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]], + ]); + + $this->container->register('app.security_listener', \stdClass::class) + ->addTag('kernel.event_listener', ['method' => 'onLogout', 'event' => LogoutEvent::class]) + ->addTag('kernel.event_listener', ['method' => 'onLoginSuccess', 'event' => LoginSuccessEvent::class, 'priority' => 20]) + ->addTag('kernel.event_listener', ['method' => 'onAuthenticationSuccess', 'event' => AuthenticationEvents::AUTHENTICATION_SUCCESS]); + + $this->container->compile(); + + $this->assertListeners([ + [LogoutEvent::class, ['app.security_listener', 'onLogout'], 0], + [LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20], + [AuthenticationEvents::AUTHENTICATION_SUCCESS, ['app.security_listener', 'onAuthenticationSuccess'], 0], + ]); + } + + public function testRegisterCustomSubscriber() + { + $this->container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]], + ]); + + $this->container->register(TestSubscriber::class) + ->addTag('kernel.event_subscriber'); + + $this->container->compile(); + + $this->assertListeners([ + [LogoutEvent::class, [TestSubscriber::class, 'onLogout'], -200], + [CheckPassportEvent::class, [TestSubscriber::class, 'onCheckPassport'], 120], + [LoginSuccessEvent::class, [TestSubscriber::class, 'onLoginSuccess'], 0], + [AuthenticationEvents::AUTHENTICATION_SUCCESS, [TestSubscriber::class, 'onAuthenticationSuccess'], 0], + ]); + } + + public function testMultipleFirewalls() + { + $this->container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true], 'api' => ['pattern' => '/api', 'http_basic' => true]], + ]); + + $this->container->register('security.event_dispatcher.api', EventDispatcher::class) + ->addTag('security.event_dispatcher') + ->setPublic(true); + + $this->container->register('app.security_listener', \stdClass::class) + ->addTag('kernel.event_listener', ['method' => 'onLogout', 'event' => LogoutEvent::class]) + ->addTag('kernel.event_listener', ['method' => 'onLoginSuccess', 'event' => LoginSuccessEvent::class, 'priority' => 20]) + ->addTag('kernel.event_listener', ['method' => 'onAuthenticationSuccess', 'event' => AuthenticationEvents::AUTHENTICATION_SUCCESS]); + + $this->container->compile(); + + $this->assertListeners([ + [LogoutEvent::class, ['app.security_listener', 'onLogout'], 0], + [LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20], + [AuthenticationEvents::AUTHENTICATION_SUCCESS, ['app.security_listener', 'onAuthenticationSuccess'], 0], + ], 'security.event_dispatcher.main'); + $this->assertListeners([ + [LogoutEvent::class, ['app.security_listener', 'onLogout'], 0], + [LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20], + [AuthenticationEvents::AUTHENTICATION_SUCCESS, ['app.security_listener', 'onAuthenticationSuccess'], 0], + ], 'security.event_dispatcher.api'); + } + + public function testListenerAlreadySpecific() + { + $this->container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => ['main' => ['pattern' => '/', 'http_basic' => true]], + ]); + + $this->container->register('security.event_dispatcher.api', EventDispatcher::class) + ->addTag('security.event_dispatcher') + ->setPublic(true); + + $this->container->register('app.security_listener', \stdClass::class) + ->addTag('kernel.event_listener', ['method' => 'onLogout', 'event' => LogoutEvent::class, 'dispatcher' => 'security.event_dispatcher.main']) + ->addTag('kernel.event_listener', ['method' => 'onLoginSuccess', 'event' => LoginSuccessEvent::class, 'priority' => 20]) + ->addTag('kernel.event_listener', ['method' => 'onAuthenticationSuccess', 'event' => AuthenticationEvents::AUTHENTICATION_SUCCESS]); + + $this->container->compile(); + + $this->assertListeners([ + [LogoutEvent::class, ['app.security_listener', 'onLogout'], 0], + [LoginSuccessEvent::class, ['app.security_listener', 'onLoginSuccess'], 20], + [AuthenticationEvents::AUTHENTICATION_SUCCESS, ['app.security_listener', 'onAuthenticationSuccess'], 0], + ], 'security.event_dispatcher.main'); + } + + private function assertListeners(array $expectedListeners, string $dispatcherId = 'security.event_dispatcher.main') + { + $actualListeners = []; + foreach ($this->container->findDefinition($dispatcherId)->getMethodCalls() as $methodCall) { + [$method, $arguments] = $methodCall; + if ('addListener' !== $method) { + continue; + } + + $arguments[1] = [(string) $arguments[1][0]->getValues()[0], $arguments[1][1]]; + $actualListeners[] = $arguments; + } + + $foundListeners = array_uintersect($expectedListeners, $actualListeners, function (array $a, array $b) { + // PHP internally sorts all the arrays first, so returning proper 1 / -1 values is crucial + return $a <=> $b; + }); + + $this->assertEquals($expectedListeners, $foundListeners); + } +} + +class TestSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents(): array + { + return [ + LogoutEvent::class => ['onLogout', -200], + CheckPassportEvent::class => ['onCheckPassport', 120], + LoginSuccessEvent::class => 'onLoginSuccess', + AuthenticationEvents::AUTHENTICATION_SUCCESS => 'onAuthenticationSuccess', + ]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php index afdbf9afaf60f..cfc363b557c2c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php @@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionFactory; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; use Symfony\Component\Security\Http\Firewall\ContextListener; @@ -41,7 +41,6 @@ public function testContextListenerIsNotModifiedIfTokenStorageDoesNotSupportUsag $container = new ContainerBuilder(); $container->setParameter('security.token_storage.class', TokenStorage::class); - $container->register('session', Session::class); $container->register('security.context_listener', ContextListener::class) ->setArguments([ new Reference('security.untracked_token_storage'), @@ -65,7 +64,7 @@ public function testContextListenerEnablesUsageTrackingIfSupportedByTokenStorage $container = new ContainerBuilder(); $container->setParameter('security.token_storage.class', UsageTrackingTokenStorage::class); - $container->register('session', Session::class); + $container->register('session.factory', SessionFactory::class); $container->register('security.context_listener', ContextListener::class) ->setArguments([ new Reference('security.untracked_token_storage'), diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/SortFirewallListenersPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/SortFirewallListenersPassTest.php new file mode 100644 index 0000000000000..8cbf745e9cc88 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/SortFirewallListenersPassTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface; + +class SortFirewallListenersPassTest extends TestCase +{ + public function testSortFirewallListeners() + { + $container = new ContainerBuilder(); + $container->setParameter('security.firewalls', ['main']); + + $container->register('listener_priority_minus1', FirewallListenerPriorityMinus1::class); + $container->register('listener_priority_1', FirewallListenerPriority1::class); + $container->register('listener_priority_2', FirewallListenerPriority2::class); + $container->register('listener_interface_not_implemented', \stdClass::class); + + $firewallContext = $container->register('security.firewall.map.context.main', FirewallContext::class); + $firewallContext->addTag('security.firewall_map_context'); + + $listeners = new IteratorArgument([ + new Reference('listener_priority_minus1'), + new Reference('listener_priority_1'), + new Reference('listener_priority_2'), + new Reference('listener_interface_not_implemented'), + ]); + + $firewallContext->setArgument(0, $listeners); + + $compilerPass = new SortFirewallListenersPass(); + $compilerPass->process($container); + + $sortedListeners = $firewallContext->getArgument(0); + $expectedSortedlisteners = [ + new Reference('listener_priority_2'), + new Reference('listener_priority_1'), + new Reference('listener_interface_not_implemented'), + new Reference('listener_priority_minus1'), + ]; + $this->assertEquals($expectedSortedlisteners, $sortedListeners->getValues()); + } +} + +class FirewallListenerPriorityMinus1 implements FirewallListenerInterface +{ + public function supports(Request $request): ?bool + { + } + + public function authenticate(RequestEvent $event) + { + } + + public static function getPriority(): int + { + return -1; + } +} + +class FirewallListenerPriority1 implements FirewallListenerInterface +{ + public function supports(Request $request): ?bool + { + } + + public function authenticate(RequestEvent $event) + { + } + + public static function getPriority(): int + { + return 1; + } +} + +class FirewallListenerPriority2 implements FirewallListenerInterface +{ + public function supports(Request $request): ?bool + { + } + + public function authenticate(RequestEvent $event) + { + } + + public static function getPriority(): int + { + return 2; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index b3cfbb9f88d71..7d8384456cea8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -17,10 +17,17 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher; +use Symfony\Component\PasswordHasher\Hasher\SodiumPasswordHasher; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; -use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; -use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; +use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; abstract class CompleteConfigurationTest extends TestCase { @@ -28,6 +35,44 @@ abstract protected function getLoader(ContainerBuilder $container); abstract protected function getFileExtension(); + public function testAuthenticatorManager() + { + $container = $this->getContainer('authenticator_manager'); + + $authenticatorManager = $container->getDefinition('security.authenticator.manager.main'); + $this->assertEquals(AuthenticatorManager::class, $authenticatorManager->getClass()); + + // required badges + $this->assertEquals([CsrfTokenBadge::class, RememberMeBadge::class], $authenticatorManager->getArgument(7)); + + // login link + $expiredStorage = $container->getDefinition($expiredStorageId = 'security.authenticator.expired_login_link_storage.main'); + $this->assertEquals('cache.redis', (string) $expiredStorage->getArgument(0)); + $this->assertEquals(3600, (string) $expiredStorage->getArgument(1)); + + $linker = $container->getDefinition($linkerId = 'security.authenticator.login_link_handler.main'); + $this->assertEquals([ + 'route_name' => 'login_check', + 'lifetime' => 3600, + ], $linker->getArgument(3)); + + $hasher = $container->getDefinition((string) $linker->getArgument(2)); + $this->assertEquals(['id', 'email'], $hasher->getArgument(1)); + $this->assertEquals($expiredStorageId, (string) $hasher->getArgument(3)); + $this->assertEquals(1, $hasher->getArgument(4)); + + $authenticator = $container->getDefinition('security.authenticator.login_link.main'); + $this->assertEquals($linkerId, (string) $authenticator->getArgument(0)); + $this->assertEquals([ + 'check_route' => 'login_check', + 'check_post_only' => true, + ], $authenticator->getArgument(4)); + + // login throttling + $listener = $container->getDefinition('security.listener.login_throttling.main'); + $this->assertEquals('app.rate_limiter', (string) $listener->getArgument(1)); + } + public function testRolesHierarchy() { $container = $this->getContainer('container1'); @@ -105,7 +150,7 @@ public function testFirewalls() true, 'security.user.provider.concrete.default', null, - 'security.authentication.form_entry_point.secure', + 'security.authenticator.form_login.secure', null, null, [ @@ -115,12 +160,10 @@ public function testFirewalls() 'form_login', 'http_basic', 'remember_me', - 'anonymous', ], [ 'parameter' => '_switch_user', 'role' => 'ROLE_ALLOWED_TO_SWITCH', - 'stateless' => false, ], ], [ @@ -131,12 +174,11 @@ public function testFirewalls() false, 'security.user.provider.concrete.default', 'host', - 'security.authentication.basic_entry_point.host', + 'security.authenticator.http_basic.host', null, null, [ 'http_basic', - 'anonymous', ], null, ], @@ -148,12 +190,11 @@ public function testFirewalls() false, 'security.user.provider.concrete.default', 'with_user_checker', - 'security.authentication.basic_entry_point.with_user_checker', + 'security.authenticator.http_basic.with_user_checker', null, null, [ 'http_basic', - 'anonymous', ], null, ], @@ -163,27 +204,20 @@ public function testFirewalls() [], [ 'security.channel_listener', - 'security.authentication.listener.x509.secure', - 'security.authentication.listener.remote_user.secure', - 'security.authentication.listener.form.secure', - 'security.authentication.listener.basic.secure', - 'security.authentication.listener.rememberme.secure', - 'security.authentication.listener.anonymous.secure', + 'security.firewall.authenticator.secure', 'security.authentication.switchuser_listener.secure', 'security.access_listener', ], [ 'security.channel_listener', 'security.context_listener.0', - 'security.authentication.listener.basic.host', - 'security.authentication.listener.anonymous.host', + 'security.firewall.authenticator.host', 'security.access_listener', ], [ 'security.channel_listener', 'security.context_listener.1', - 'security.authentication.listener.basic.with_user_checker', - 'security.authentication.listener.anonymous.with_user_checker', + 'security.firewall.authenticator.with_user_checker', 'security.access_listener', ], ], $listeners); @@ -261,7 +295,7 @@ public function testAccess() } elseif (3 === $i) { $this->assertEquals('IS_AUTHENTICATED_ANONYMOUSLY', $attributes[0]); $expression = $container->getDefinition((string) $attributes[1])->getArgument(0); - $this->assertEquals("token.getUsername() matches '/^admin/'", $expression); + $this->assertEquals("token.getUserIdentifier() matches '/^admin/'", $expression); } } } @@ -276,13 +310,13 @@ public function testMerge() ], $container->getParameter('security.role_hierarchy.roles')); } - public function testEncoders() + public function testHashers() { $container = $this->getContainer('container1'); $this->assertEquals([[ 'JMS\FooBundle\Entity\User1' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'class' => PlaintextPasswordHasher::class, 'arguments' => [false], ], 'JMS\FooBundle\Entity\User2' => [ @@ -295,7 +329,6 @@ public function testEncoders() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ @@ -308,16 +341,15 @@ public function testEncoders() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], - 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'), 'JMS\FooBundle\Entity\User5' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'class' => Pbkdf2PasswordHasher::class, 'arguments' => ['sha1', false, 5, 30], ], 'JMS\FooBundle\Entity\User6' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'class' => NativePasswordHasher::class, 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ @@ -330,23 +362,22 @@ public function testEncoders() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], - ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + ]], $container->getDefinition('security.password_hasher_factory')->getArguments()); } - public function testEncodersWithLibsodium() + public function testHashersWithLibsodium() { - if (!SodiumPasswordEncoder::isSupported()) { + if (!SodiumPasswordHasher::isSupported()) { $this->markTestSkipped('Libsodium is not available.'); } - $container = $this->getContainer('sodium_encoder'); + $container = $this->getContainer('sodium_hasher'); $this->assertEquals([[ 'JMS\FooBundle\Entity\User1' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'class' => PlaintextPasswordHasher::class, 'arguments' => [false], ], 'JMS\FooBundle\Entity\User2' => [ @@ -359,7 +390,6 @@ public function testEncodersWithLibsodium() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ @@ -372,36 +402,35 @@ public function testEncodersWithLibsodium() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], - 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'), 'JMS\FooBundle\Entity\User5' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'class' => Pbkdf2PasswordHasher::class, 'arguments' => ['sha1', false, 5, 30], ], 'JMS\FooBundle\Entity\User6' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'class' => NativePasswordHasher::class, 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder', + 'class' => SodiumPasswordHasher::class, 'arguments' => [8, 128 * 1024 * 1024], ], - ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + ]], $container->getDefinition('security.password_hasher_factory')->getArguments()); } - public function testEncodersWithArgon2i() + public function testHashersWithArgon2i() { - if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { + if (!($sodium = SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } - $container = $this->getContainer('argon2i_encoder'); + $container = $this->getContainer('argon2i_hasher'); $this->assertEquals([[ 'JMS\FooBundle\Entity\User1' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'class' => PlaintextPasswordHasher::class, 'arguments' => [false], ], 'JMS\FooBundle\Entity\User2' => [ @@ -414,7 +443,6 @@ public function testEncodersWithArgon2i() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ @@ -427,36 +455,35 @@ public function testEncodersWithArgon2i() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], - 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'), 'JMS\FooBundle\Entity\User5' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'class' => Pbkdf2PasswordHasher::class, 'arguments' => ['sha1', false, 5, 30], ], 'JMS\FooBundle\Entity\User6' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'class' => NativePasswordHasher::class, 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => $sodium ? SodiumPasswordEncoder::class : NativePasswordEncoder::class, + 'class' => $sodium ? SodiumPasswordHasher::class : NativePasswordHasher::class, 'arguments' => $sodium ? [256, 1] : [1, 262144, null, \PASSWORD_ARGON2I], ], - ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + ]], $container->getDefinition('security.password_hasher_factory')->getArguments()); } - public function testMigratingEncoder() + public function testMigratingHasher() { - if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { + if (!($sodium = SodiumPasswordHasher::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } - $container = $this->getContainer('migrating_encoder'); + $container = $this->getContainer('migrating_hasher'); $this->assertEquals([[ 'JMS\FooBundle\Entity\User1' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'class' => PlaintextPasswordHasher::class, 'arguments' => [false], ], 'JMS\FooBundle\Entity\User2' => [ @@ -469,7 +496,6 @@ public function testMigratingEncoder() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ @@ -482,16 +508,15 @@ public function testMigratingEncoder() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], - 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'), 'JMS\FooBundle\Entity\User5' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'class' => Pbkdf2PasswordHasher::class, 'arguments' => ['sha1', false, 5, 30], ], 'JMS\FooBundle\Entity\User6' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'class' => NativePasswordHasher::class, 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ @@ -504,19 +529,18 @@ public function testMigratingEncoder() 'cost' => null, 'memory_cost' => 256, 'time_cost' => 1, - 'threads' => null, 'migrate_from' => ['bcrypt'], ], - ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + ]], $container->getDefinition('security.password_hasher_factory')->getArguments()); } - public function testEncodersWithBCrypt() + public function testHashersWithBCrypt() { - $container = $this->getContainer('bcrypt_encoder'); + $container = $this->getContainer('bcrypt_hasher'); $this->assertEquals([[ 'JMS\FooBundle\Entity\User1' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'class' => PlaintextPasswordHasher::class, 'arguments' => [false], ], 'JMS\FooBundle\Entity\User2' => [ @@ -529,7 +553,6 @@ public function testEncodersWithBCrypt() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ @@ -542,37 +565,22 @@ public function testEncodersWithBCrypt() 'cost' => null, 'memory_cost' => null, 'time_cost' => null, - 'threads' => null, 'migrate_from' => [], ], - 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User4' => new Reference('security.hasher.foo'), 'JMS\FooBundle\Entity\User5' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'class' => Pbkdf2PasswordHasher::class, 'arguments' => ['sha1', false, 5, 30], ], 'JMS\FooBundle\Entity\User6' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'class' => NativePasswordHasher::class, 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => NativePasswordEncoder::class, + 'class' => NativePasswordHasher::class, 'arguments' => [null, null, 15, \PASSWORD_BCRYPT], ], - ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); - } - - public function testRememberMeThrowExceptionsDefault() - { - $container = $this->getContainer('container1'); - $this->assertTrue($container->getDefinition('security.authentication.listener.rememberme.secure')->getArgument(5)); - } - - public function testRememberMeThrowExceptions() - { - $container = $this->getContainer('remember_me_options'); - $service = $container->getDefinition('security.authentication.listener.rememberme.main'); - $this->assertEquals('security.authentication.rememberme.services.persistent.main', $service->getArgument(1)); - $this->assertFalse($service->getArgument(5)); + ]], $container->getDefinition('security.password_hasher_factory')->getArguments()); } public function testUserCheckerConfig() @@ -590,16 +598,16 @@ public function testUserCheckerConfigWithNoCheckers() $this->assertEquals('security.user_checker', $this->getContainer('container1')->getAlias('security.user_checker.secure')); } - public function testUserPasswordEncoderCommandIsRegistered() + public function testUserPasswordHasherCommandIsRegistered() { - $this->assertTrue($this->getContainer('remember_me_options')->has('security.command.user_password_encoder')); + $this->assertTrue($this->getContainer('remember_me_options')->has('security.command.user_password_hash')); } public function testDefaultAccessDecisionManagerStrategyIsAffirmative() { $container = $this->getContainer('access_decision_manager_default_strategy'); - $this->assertSame(AccessDecisionManager::STRATEGY_AFFIRMATIVE, $container->getDefinition('security.access.decision_manager')->getArgument(1), 'Default vote strategy is affirmative'); + $this->assertEquals((new Definition(AffirmativeStrategy::class, [false])), $container->getDefinition('security.access.decision_manager')->getArgument(1), 'Default vote strategy is affirmative'); } public function testCustomAccessDecisionManagerService() @@ -622,9 +630,17 @@ public function testAccessDecisionManagerOptionsAreNotOverriddenByImplicitStrate $accessDecisionManagerDefinition = $container->getDefinition('security.access.decision_manager'); - $this->assertSame(AccessDecisionManager::STRATEGY_AFFIRMATIVE, $accessDecisionManagerDefinition->getArgument(1)); - $this->assertTrue($accessDecisionManagerDefinition->getArgument(2)); - $this->assertFalse($accessDecisionManagerDefinition->getArgument(3)); + $this->assertEquals((new Definition(AffirmativeStrategy::class, [true])), $accessDecisionManagerDefinition->getArgument(1)); + } + + public function testAccessDecisionManagerWithStrategyService() + { + $container = $this->getContainer('access_decision_manager_strategy_service'); + + $accessDecisionManagerDefinition = $container->getDefinition('security.access.decision_manager'); + + $this->assertEquals(AccessDecisionManager::class, $accessDecisionManagerDefinition->getClass()); + $this->assertEquals(new Reference('app.custom_access_decision_strategy'), $accessDecisionManagerDefinition->getArgument(1)); } public function testFirewallUndefinedUserProvider() @@ -653,64 +669,6 @@ public function testFirewallListenerWithProvider() $this->addToAssertionCount(1); } - /** - * @group legacy - * @expectedDeprecation The "simple_form" security listener is deprecated Symfony 4.2, use Guard instead. - */ - public function testSimpleAuth() - { - $container = $this->getContainer('simple_auth'); - $arguments = $container->getDefinition('security.firewall.map')->getArguments(); - $listeners = []; - $configs = []; - foreach (array_keys($arguments[1]->getValues()) as $contextId) { - $contextDef = $container->getDefinition($contextId); - $arguments = $contextDef->getArguments(); - $listeners[] = array_map('strval', $arguments[0]->getValues()); - - $configDef = $container->getDefinition((string) $arguments[3]); - $configs[] = array_values($configDef->getArguments()); - } - - $this->assertSame([[ - 'simple_auth', - 'security.user_checker', - null, - true, - false, - 'security.user.provider.concrete.default', - 'simple_auth', - 'security.authentication.form_entry_point.simple_auth', - null, - null, - ['simple_form', 'anonymous', - ], - null, - ]], $configs); - - $this->assertSame([[ - 'security.channel_listener', - 'security.context_listener.0', - 'security.authentication.listener.simple_form.simple_auth', - 'security.authentication.listener.anonymous.simple_auth', - 'security.access_listener', - ]], $listeners); - } - - /** - * @group legacy - * @expectedDeprecation Normalization of cookie names is deprecated since Symfony 4.3. Starting from Symfony 5.0, the "cookie1-name" cookie configured in "logout.delete_cookies" will delete the "cookie1-name" cookie instead of the "cookie1_name" cookie. - * @expectedDeprecation Normalization of cookie names is deprecated since Symfony 4.3. Starting from Symfony 5.0, the "cookie3-long_name" cookie configured in "logout.delete_cookies" will delete the "cookie3-long_name" cookie instead of the "cookie3_long_name" cookie. - */ - public function testLogoutDeleteCookieNamesNormalization() - { - $container = $this->getContainer('logout_delete_cookies'); - $cookiesToDelete = $container->getDefinition('security.logout.handler.cookie_clearing.main')->getArgument(0); - $expectedCookieNames = ['cookie2_name', 'cookie1_name', 'cookie3_long_name']; - - $this->assertSame($expectedCookieNames, array_keys($cookiesToDelete)); - } - protected function getContainer($file) { $file .= '.'.$this->getFileExtension(); @@ -719,6 +677,7 @@ protected function getContainer($file) $container->setParameter('kernel.debug', false); $container->setParameter('request_listener.http_port', 80); $container->setParameter('request_listener.https_port', 443); + $container->register('cache.app', \stdClass::class); $security = new SecurityExtension(); $container->registerExtension($security); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_customized_config.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_customized_config.php index 1d0a090f3f589..6d011aebd5998 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_customized_config.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_customized_config.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'access_decision_manager' => [ 'allow_if_all_abstain' => true, 'allow_if_equal_granted_denied' => false, diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_default_strategy.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_default_strategy.php index 1f0adbf3010f1..cfa7751b7b4bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_default_strategy.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_default_strategy.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => [ 'memory' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service.php index 8f615904ddf0d..dee30bedc9c9f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'access_decision_manager' => [ 'service' => 'app.access_decision_manager', ], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service_and_strategy.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service_and_strategy.php index bd78bdf24d578..d964561c42657 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service_and_strategy.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_service_and_strategy.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'access_decision_manager' => [ 'service' => 'app.access_decision_manager', 'strategy' => 'affirmative', diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_strategy_service.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_strategy_service.php new file mode 100644 index 0000000000000..8024e3a72f25b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/access_decision_manager_strategy_service.php @@ -0,0 +1,20 @@ +loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'access_decision_manager' => [ + 'strategy_service' => 'app.custom_access_decision_strategy', + ], + 'providers' => [ + 'default' => [ + 'memory' => [ + 'users' => [ + 'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'], + ], + ], + ], + ], + 'firewalls' => [ + 'simple' => ['pattern' => '/login', 'security' => false], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php deleted file mode 100644 index ddac043692cf1..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php +++ /dev/null @@ -1,13 +0,0 @@ -load('container1.php'); - -$container->loadFromExtension('security', [ - 'encoders' => [ - 'JMS\FooBundle\Entity\User7' => [ - 'algorithm' => 'argon2i', - 'memory_cost' => 256, - 'time_cost' => 1, - ], - ], -]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_hasher.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_hasher.php new file mode 100644 index 0000000000000..6254f5747841f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_hasher.php @@ -0,0 +1,14 @@ +load('container1.php'); + +$container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'password_hashers' => [ + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'argon2i', + 'memory_cost' => 256, + 'time_cost' => 1, + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php new file mode 100644 index 0000000000000..fa53fb980f67a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php @@ -0,0 +1,23 @@ +loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + 'main' => [ + 'required_badges' => [CsrfTokenBadge::class, 'RememberMeBadge'], + 'login_link' => [ + 'check_route' => 'login_check', + 'check_post_only' => true, + 'signature_properties' => ['id', 'email'], + 'max_uses' => 1, + 'lifetime' => 3600, + 'used_link_cache' => 'cache.redis', + ], + 'login_throttling' => [ + 'limiter' => 'app.rate_limiter', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php deleted file mode 100644 index d4511aeb554c7..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php +++ /dev/null @@ -1,12 +0,0 @@ -load('container1.php'); - -$container->loadFromExtension('security', [ - 'encoders' => [ - 'JMS\FooBundle\Entity\User7' => [ - 'algorithm' => 'bcrypt', - 'cost' => 15, - ], - ], -]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_hasher.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_hasher.php new file mode 100644 index 0000000000000..ac29b31e096e6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_hasher.php @@ -0,0 +1,13 @@ +load('container1.php'); + +$container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'password_hashers' => [ + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'bcrypt', + 'cost' => 15, + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php index 3c9e6104eecc3..f155c135a26ad 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php @@ -1,7 +1,8 @@ loadFromExtension('security', [ - 'encoders' => [ + 'enable_authenticator_manager' => true, + 'password_hashers' => [ 'JMS\FooBundle\Entity\User1' => 'plaintext', 'JMS\FooBundle\Entity\User2' => [ 'algorithm' => 'sha1', @@ -12,7 +13,7 @@ 'algorithm' => 'md5', ], 'JMS\FooBundle\Entity\User4' => [ - 'id' => 'security.encoder.foo', + 'id' => 'security.hasher.foo', ], 'JMS\FooBundle\Entity\User5' => [ 'algorithm' => 'pbkdf2', @@ -70,26 +71,24 @@ 'provider' => 'default', 'http_basic' => true, 'form_login' => true, - 'anonymous' => true, 'switch_user' => true, 'x509' => true, 'remote_user' => true, 'logout' => true, 'remember_me' => ['secret' => 'TheSecret'], 'user_checker' => null, + 'entry_point' => 'form_login', ], 'host' => [ 'provider' => 'default', 'pattern' => '/test', 'host' => 'foo\\.example\\.org', 'methods' => ['GET', 'POST'], - 'anonymous' => true, 'http_basic' => true, ], 'with_user_checker' => [ 'provider' => 'default', 'user_checker' => 'app.user_checker', - 'anonymous' => true, 'http_basic' => true, ], ], @@ -97,7 +96,7 @@ 'access_control' => [ ['path' => '/blog/524', 'role' => 'ROLE_USER', 'requires_channel' => 'https', 'methods' => ['get', 'POST'], 'port' => 8000], ['path' => '/blog/.*', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'], - ['path' => '/blog/524', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'allow_if' => "token.getUsername() matches '/^admin/'"], + ['path' => '/blog/524', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'allow_if' => "token.getUserIdentifier() matches '/^admin/'"], ], 'role_hierarchy' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_provider.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_provider.php index 68b8439a7de5a..eeec20726a588 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_provider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_provider.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => [ 'memory' => $memory = [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_undefined_provider.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_undefined_provider.php index 7c811cae1a4dd..dd90214810572 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_undefined_provider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/firewall_undefined_provider.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => [ 'memory' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/legacy_remember_me_options.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/legacy_remember_me_options.php new file mode 100644 index 0000000000000..cfbef609a18db --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/legacy_remember_me_options.php @@ -0,0 +1,18 @@ +loadFromExtension('security', [ + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + + 'firewalls' => [ + 'main' => [ + 'form_login' => true, + 'remember_me' => [ + 'secret' => 'TheSecret', + 'catch_exceptions' => false, + 'token_provider' => 'token_provider_id', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_provider.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_provider.php index 0a6a79f5f208c..8ddc21f13edc3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_provider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_provider.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => [ 'memory' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_undefined_provider.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_undefined_provider.php index cc0b776e432c4..10661fae2010b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_undefined_provider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/listener_undefined_provider.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => [ 'memory' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/logout_delete_cookies.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/logout_delete_cookies.php index 8ffe12e3eb929..7a40881b655d1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/logout_delete_cookies.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/logout_delete_cookies.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php index d0bd809579e89..03a5f1d28c87c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php @@ -3,6 +3,7 @@ $this->load('merge_import.php'); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php index c85937d6ea2c9..198935390cfd1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'firewalls' => [ 'main' => [ 'form_login' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_encoder.php deleted file mode 100644 index c7ad9f02ab4f5..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_encoder.php +++ /dev/null @@ -1,14 +0,0 @@ -load('container1.php'); - -$container->loadFromExtension('security', [ - 'encoders' => [ - 'JMS\FooBundle\Entity\User7' => [ - 'algorithm' => 'argon2i', - 'memory_cost' => 256, - 'time_cost' => 1, - 'migrate_from' => 'bcrypt', - ], - ], -]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_hasher.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_hasher.php new file mode 100644 index 0000000000000..3f68562c8a07c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_hasher.php @@ -0,0 +1,15 @@ +load('container1.php'); + +$container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'password_hashers' => [ + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'argon2i', + 'memory_cost' => 256, + 'time_cost' => 1, + 'migrate_from' => 'bcrypt', + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/no_custom_user_checker.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/no_custom_user_checker.php index 7565452eb5286..29d93a1f2ec3e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/no_custom_user_checker.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/no_custom_user_checker.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => [ 'memory' => [ @@ -16,13 +17,13 @@ 'stateless' => true, 'http_basic' => true, 'form_login' => true, - 'anonymous' => true, 'switch_user' => true, 'x509' => true, 'remote_user' => true, 'logout' => true, 'remember_me' => ['secret' => 'TheSecret'], 'user_checker' => null, + 'entry_point' => 'form_login', ], ], ]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/remember_me_options.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/remember_me_options.php index cfbef609a18db..0e8963f9297e3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/remember_me_options.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/remember_me_options.php @@ -1,6 +1,7 @@ loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/simple_auth.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/simple_auth.php deleted file mode 100644 index 05829defd119d..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/simple_auth.php +++ /dev/null @@ -1,21 +0,0 @@ -loadFromExtension('security', [ - 'providers' => [ - 'default' => [ - 'memory' => [ - 'users' => [ - 'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'], - ], - ], - ], - ], - - 'firewalls' => [ - 'simple_auth' => [ - 'provider' => 'default', - 'anonymous' => true, - 'simple_form' => ['authenticator' => 'simple_authenticator'], - ], - ], -]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php deleted file mode 100644 index ec0851bdfaa34..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_encoder.php +++ /dev/null @@ -1,13 +0,0 @@ -load('container1.php'); - -$container->loadFromExtension('security', [ - 'encoders' => [ - 'JMS\FooBundle\Entity\User7' => [ - 'algorithm' => 'sodium', - 'time_cost' => 8, - 'memory_cost' => 128 * 1024, - ], - ], -]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_hasher.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_hasher.php new file mode 100644 index 0000000000000..8f17965b25bd6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/sodium_hasher.php @@ -0,0 +1,14 @@ +load('container1.php'); + +$container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'password_hashers' => [ + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'sodium', + 'time_cost' => 8, + 'memory_cost' => 128 * 1024, + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_customized_config.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_customized_config.xml index 0b6861fd9cdb6..9116042908d12 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_customized_config.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_customized_config.xml @@ -2,14 +2,17 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_default_strategy.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_default_strategy.xml index 657f3c4986c06..85c8050cbc70f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_default_strategy.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_default_strategy.xml @@ -2,12 +2,15 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service.xml index fb51a7413a45d..3e189b8c61ff6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service.xml @@ -2,14 +2,17 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service_and_strategy.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service_and_strategy.xml index 460b44cda03d6..5b70a4614addb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service_and_strategy.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_service_and_strategy.xml @@ -2,14 +2,17 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_strategy_service.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_strategy_service.xml new file mode 100644 index 0000000000000..94763b543f4ec --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access_decision_manager_strategy_service.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml deleted file mode 100644 index 6a7c2a5041cdb..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml new file mode 100644 index 0000000000000..8168af333e13d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml new file mode 100644 index 0000000000000..0185b81c440c8 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml @@ -0,0 +1,26 @@ + + + + + + Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge + RememberMeBadge + + id + email + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/bcrypt_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/bcrypt_encoder.xml deleted file mode 100644 index a98400c5f043a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/bcrypt_encoder.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml new file mode 100644 index 0000000000000..a1f784ed96761 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml index c919a7f276732..c97dd5bf7ebf0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml @@ -3,39 +3,42 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - - + + - + - + - + - + - + - + - + - + - - + + @@ -47,25 +50,21 @@ - + - - - - app.user_checker @@ -76,6 +75,6 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml index 77220a1f9d0a6..6f74984045970 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml @@ -1,11 +1,14 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:sec="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml index ad209f0b0d72e..a80f613e00331 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml @@ -1,11 +1,14 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:sec="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/legacy_remember_me_options.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/legacy_remember_me_options.xml new file mode 100644 index 0000000000000..767397ada3515 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/legacy_remember_me_options.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml index 3ad9efc24c02f..b45f378a5ba68 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml @@ -1,11 +1,14 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:sec="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml index 98b3dbe2f5c67..bdf9d5ec837f0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml @@ -1,11 +1,14 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:sec="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml index 34b4a429e5fc3..e817b48901311 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml @@ -1,21 +1,22 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:srv="http://symfony.com/schema/dic/services" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - - - - - + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml index bd03d6229c158..569e20e65e3b9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml @@ -3,13 +3,16 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml index 441dd6fcda36b..c7c237f2fefa4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml @@ -3,9 +3,12 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_encoder.xml deleted file mode 100644 index d820118075108..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_encoder.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - bcrypt - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml new file mode 100644 index 0000000000000..d0d0b4ff91ea7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + bcrypt + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml index 84bdfef7fcf6d..c4dea529ba452 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/no_custom_user_checker.xml @@ -2,21 +2,23 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - + - + - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml index d833cf8fdefdd..9921d6c5fe6b0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml @@ -1,13 +1,16 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:sec="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/services + https://symfony.com/schema/dic/services/services-1.0.xsd + http://symfony.com/schema/dic/security + https://symfony.com/schema/dic/security/security-1.0.xsd"> - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/simple_auth.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/simple_auth.xml deleted file mode 100644 index 85fb312011bea..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/simple_auth.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml deleted file mode 100644 index 11682f7c950fc..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_encoder.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml new file mode 100644 index 0000000000000..67d4d1304b31e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_customized_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_customized_config.yml index a8d044f1dec5d..db0f2b551cf92 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_customized_config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_customized_config.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true access_decision_manager: allow_if_all_abstain: true allow_if_equal_granted_denied: false diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_default_strategy.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_default_strategy.yml index f7fb5adc2c5d4..adfeffa5fb8c3 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_default_strategy.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_default_strategy.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true providers: default: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service.yml index 7ef3d8d93c3ab..b162a45916194 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true access_decision_manager: service: app.access_decision_manager providers: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service_and_strategy.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service_and_strategy.yml index bd38b21ef3536..ced97bb5337b8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service_and_strategy.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_service_and_strategy.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true access_decision_manager: service: app.access_decision_manager strategy: affirmative diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_strategy_service.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_strategy_service.yml new file mode 100644 index 0000000000000..907cdfe8410c6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/access_decision_manager_strategy_service.yml @@ -0,0 +1,11 @@ +security: + enable_authenticator_manager: true + access_decision_manager: + strategy_service: app.custom_access_decision_strategy + providers: + default: + memory: + users: + foo: { password: foo, roles: ROLE_USER } + firewalls: + simple: { pattern: /login, security: false } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml deleted file mode 100644 index cadf8eb1e98d2..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml +++ /dev/null @@ -1,9 +0,0 @@ -imports: - - { resource: container1.yml } - -security: - encoders: - JMS\FooBundle\Entity\User7: - algorithm: argon2i - memory_cost: 256 - time_cost: 1 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_hasher.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_hasher.yml new file mode 100644 index 0000000000000..0ae8214f1246e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_hasher.yml @@ -0,0 +1,10 @@ +imports: + - { resource: container1.yml } + +security: + enable_authenticator_manager: true + password_hashers: + JMS\FooBundle\Entity\User7: + algorithm: argon2i + memory_cost: 256 + time_cost: 1 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml new file mode 100644 index 0000000000000..7efae5356f0f4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml @@ -0,0 +1,16 @@ +security: + enable_authenticator_manager: true + firewalls: + main: + required_badges: + - 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge' + - RememberMeBadge + login_link: + check_route: login_check + check_post_only: true + signature_properties: [id, email] + max_uses: 1 + lifetime: 3600 + used_link_cache: 'cache.redis' + login_throttling: + limiter: 'app.rate_limiter' diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/bcrypt_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/bcrypt_encoder.yml deleted file mode 100644 index 3f1a526215204..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/bcrypt_encoder.yml +++ /dev/null @@ -1,8 +0,0 @@ -imports: - - { resource: container1.yml } - -security: - encoders: - JMS\FooBundle\Entity\User7: - algorithm: bcrypt - cost: 15 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/bcrypt_hasher.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/bcrypt_hasher.yml new file mode 100644 index 0000000000000..c8a4a71ce4667 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/bcrypt_hasher.yml @@ -0,0 +1,9 @@ +imports: + - { resource: container1.yml } + +security: + enable_authenticator_manager: true + password_hashers: + JMS\FooBundle\Entity\User7: + algorithm: bcrypt + cost: 15 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml index 03b9aaf6ef5b9..16de382cc1f2f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml @@ -1,5 +1,6 @@ security: - encoders: + enable_authenticator_manager: true + password_hashers: JMS\FooBundle\Entity\User1: plaintext JMS\FooBundle\Entity\User2: algorithm: sha1 @@ -8,7 +9,7 @@ security: JMS\FooBundle\Entity\User3: algorithm: md5 JMS\FooBundle\Entity\User4: - id: security.encoder.foo + id: security.hasher.foo JMS\FooBundle\Entity\User5: algorithm: pbkdf2 hash_algorithm: sha1 @@ -51,7 +52,6 @@ security: stateless: true http_basic: true form_login: true - anonymous: true switch_user: x509: true remote_user: true @@ -59,18 +59,17 @@ security: remember_me: secret: TheSecret user_checker: ~ + entry_point: form_login host: provider: default pattern: /test host: foo\.example\.org methods: [GET,POST] - anonymous: true http_basic: true with_user_checker: provider: default - anonymous: ~ http_basic: ~ user_checker: app.user_checker @@ -84,4 +83,4 @@ security: - path: /blog/.* role: IS_AUTHENTICATED_ANONYMOUSLY - - { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUsername() matches '/^admin/'" } + - { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUserIdentifier() matches '/^admin/'" } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_provider.yml index 11c329aa8e2fe..9aa008a75d302 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_provider.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_provider.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true providers: default: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_undefined_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_undefined_provider.yml index ec2664054009c..e10a2eaf398b1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_undefined_provider.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/firewall_undefined_provider.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true providers: default: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/legacy_remember_me_options.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/legacy_remember_me_options.yml new file mode 100644 index 0000000000000..a521c8c6a803d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/legacy_remember_me_options.yml @@ -0,0 +1,12 @@ +security: + providers: + default: + id: foo + + firewalls: + main: + form_login: true + remember_me: + secret: TheSecret + catch_exceptions: false + token_provider: token_provider_id diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_provider.yml index 652f23b5f0425..c3c1c282898a0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_provider.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_provider.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true providers: default: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_undefined_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_undefined_provider.yml index 1916df4c2e7ca..3cab5355ddd4e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_undefined_provider.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/listener_undefined_provider.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true providers: default: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/logout_delete_cookies.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/logout_delete_cookies.yml index 09bea8c13ab37..a94bc1ff8f32b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/logout_delete_cookies.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/logout_delete_cookies.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true providers: default: id: foo diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml index 60c0bbea558e7..50ae533138613 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml @@ -2,6 +2,7 @@ imports: - { resource: merge_import.yml } security: + enable_authenticator_manager: true providers: default: { id: foo } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml index 4f8db0a09f7b4..bf91f016a29c4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml @@ -1,4 +1,5 @@ security: + enable_authenticator_manager: true firewalls: main: form_login: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_encoder.yml deleted file mode 100644 index 9eda61c18866f..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_encoder.yml +++ /dev/null @@ -1,10 +0,0 @@ -imports: - - { resource: container1.yml } - -security: - encoders: - JMS\FooBundle\Entity\User7: - algorithm: argon2i - memory_cost: 256 - time_cost: 1 - migrate_from: bcrypt diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_hasher.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_hasher.yml new file mode 100644 index 0000000000000..60ac97f48fce9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_hasher.yml @@ -0,0 +1,11 @@ +imports: + - { resource: container1.yml } + +security: + enable_authenticator_manager: true + password_hashers: + JMS\FooBundle\Entity\User7: + algorithm: argon2i + memory_cost: 256 + time_cost: 1 + migrate_from: bcrypt diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/no_custom_user_checker.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/no_custom_user_checker.yml index 6a196597c51e7..d42c45edb0a31 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/no_custom_user_checker.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/no_custom_user_checker.yml @@ -1,4 +1,6 @@ security: + enable_authenticator_manager: true + providers: default: memory: @@ -11,7 +13,6 @@ security: stateless: true http_basic: true form_login: true - anonymous: true switch_user: true x509: true remote_user: true @@ -19,3 +20,4 @@ security: remember_me: secret: TheSecret user_checker: ~ + entry_point: form_login diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/remember_me_options.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/remember_me_options.yml index a521c8c6a803d..b4a1a8f6e49b5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/remember_me_options.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/remember_me_options.yml @@ -1,4 +1,6 @@ security: + enable_authenticator_manager: true + providers: default: id: foo diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/simple_auth.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/simple_auth.yml deleted file mode 100644 index b0f19e869b67c..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/simple_auth.yml +++ /dev/null @@ -1,12 +0,0 @@ -security: - providers: - default: - memory: - users: - foo: { password: foo, roles: ROLE_USER } - - firewalls: - simple_auth: - provider: default - anonymous: ~ - simple_form: { authenticator: simple_authenticator } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml deleted file mode 100644 index 2d70ef0d9b42a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_encoder.yml +++ /dev/null @@ -1,9 +0,0 @@ -imports: - - { resource: container1.yml } - -security: - encoders: - JMS\FooBundle\Entity\User7: - algorithm: sodium - time_cost: 8 - memory_cost: 131072 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_hasher.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_hasher.yml new file mode 100644 index 0000000000000..7c417bfe71d08 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/sodium_hasher.yml @@ -0,0 +1,10 @@ +imports: + - { resource: container1.yml } + +security: + enable_authenticator_manager: true + password_hashers: + JMS\FooBundle\Entity\User7: + algorithm: sodium + time_cost: 8 + memory_cost: 131072 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index ffefe42ccb8d9..cec8b019b2000 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -13,9 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; class MainConfigurationTest extends TestCase { @@ -119,7 +119,7 @@ public function testConfigMergeWithAccessDecisionManager() { $config = [ 'access_decision_manager' => [ - 'strategy' => AccessDecisionManager::STRATEGY_UNANIMOUS, + 'strategy' => MainConfiguration::STRATEGY_UNANIMOUS, ], ]; $config = array_merge(static::$minimalConfig, $config); @@ -130,6 +130,16 @@ public function testConfigMergeWithAccessDecisionManager() $configuration = new MainConfiguration([], []); $processedConfig = $processor->processConfiguration($configuration, [$config, $config2]); - $this->assertSame(AccessDecisionManager::STRATEGY_UNANIMOUS, $processedConfig['access_decision_manager']['strategy']); + $this->assertSame(MainConfiguration::STRATEGY_UNANIMOUS, $processedConfig['access_decision_manager']['strategy']); + } + + public function testFirewalls() + { + $factory = $this->createMock(AuthenticatorFactoryInterface::class); + $factory->expects($this->once())->method('addConfiguration'); + $factory->method('getKey')->willReturn('key'); + + $configuration = new MainConfiguration(['stub' => $factory], []); + $configuration->getConfigTreeBuilder(); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php index c12e0c1e950bd..ba1a1f8428c6b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php @@ -14,38 +14,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; class AbstractFactoryTest extends TestCase { - public function testCreate() + private $container; + + protected function setUp(): void { - [$container, $authProviderId, $listenerId, $entryPointId] = $this->callFactory('foo', [ - 'use_forward' => true, - 'failure_path' => '/foo', - 'success_handler' => 'custom_success_handler', - 'failure_handler' => 'custom_failure_handler', - 'remember_me' => true, - ], 'user_provider', 'entry_point'); - - // auth provider - $this->assertEquals('auth_provider', $authProviderId); - - // listener - $this->assertEquals('abstract_listener.foo', $listenerId); - $this->assertTrue($container->hasDefinition('abstract_listener.foo')); - $definition = $container->getDefinition('abstract_listener.foo'); - $this->assertEquals([ - 'index_4' => 'foo', - 'index_5' => new Reference('security.authentication.success_handler.foo.abstract_factory'), - 'index_6' => new Reference('security.authentication.failure_handler.foo.abstract_factory'), - 'index_7' => [ - 'use_forward' => true, - ], - ], $definition->getArguments()); - - // entry point - $this->assertEquals('entry_point', $entryPointId, '->create() does not change the default entry point.'); + $this->container = new ContainerBuilder(); } /** @@ -60,14 +36,12 @@ public function testDefaultFailureHandler($serviceId, $defaultHandlerInjection) if ($serviceId) { $options['failure_handler'] = $serviceId; + $this->container->register($serviceId, \stdClass::class); } - [$container] = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); + $this->callFactory('foo', $options, 'user_provider', 'entry_point'); - $definition = $container->getDefinition('abstract_listener.foo'); - $arguments = $definition->getArguments(); - $this->assertEquals(new Reference('security.authentication.failure_handler.foo.abstract_factory'), $arguments['index_6']); - $failureHandler = $container->findDefinition((string) $arguments['index_6']); + $failureHandler = $this->container->getDefinition('security.authentication.failure_handler.foo.stub'); $methodCalls = $failureHandler->getMethodCalls(); if ($defaultHandlerInjection) { @@ -98,20 +72,18 @@ public function testDefaultSuccessHandler($serviceId, $defaultHandlerInjection) if ($serviceId) { $options['success_handler'] = $serviceId; + $this->container->register($serviceId, \stdClass::class); } - [$container] = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); + $this->callFactory('foo', $options, 'user_provider', 'entry_point'); - $definition = $container->getDefinition('abstract_listener.foo'); - $arguments = $definition->getArguments(); - $this->assertEquals(new Reference('security.authentication.success_handler.foo.abstract_factory'), $arguments['index_5']); - $successHandler = $container->findDefinition((string) $arguments['index_5']); + $successHandler = $this->container->getDefinition('security.authentication.success_handler.foo.stub'); $methodCalls = $successHandler->getMethodCalls(); if ($defaultHandlerInjection) { $this->assertEquals('setOptions', $methodCalls[0][0]); $this->assertEquals(['default_target_path' => '/bar'], $methodCalls[0][1][0]); - $this->assertEquals('setProviderKey', $methodCalls[1][0]); + $this->assertEquals('setFirewallName', $methodCalls[1][0]); $this->assertEquals(['foo'], $methodCalls[1][1]); } else { $this->assertCount(0, $methodCalls); @@ -126,33 +98,29 @@ public function getSuccessHandlers() ]; } - protected function callFactory($id, $config, $userProviderId, $defaultEntryPointId) + protected function callFactory(string $firewallName, array $config, string $userProviderId, string $defaultEntryPointId) + { + (new StubFactory())->createAuthenticator($this->container, $firewallName, $config, $userProviderId); + } +} + +class StubFactory extends AbstractFactory +{ + public function getPriority(): int + { + return 0; + } + + public function getKey(): string { - $factory = $this->getMockForAbstractClass(AbstractFactory::class); - - $factory - ->expects($this->once()) - ->method('createAuthProvider') - ->willReturn('auth_provider') - ; - $factory - ->expects($this->atLeastOnce()) - ->method('getListenerId') - ->willReturn('abstract_listener') - ; - $factory - ->expects($this->any()) - ->method('getKey') - ->willReturn('abstract_factory') - ; - - $container = new ContainerBuilder(); - $container->register('auth_provider'); - $container->register('custom_success_handler'); - $container->register('custom_failure_handler'); - - [$authProviderId, $listenerId, $entryPointId] = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); - - return [$container, $authProviderId, $listenerId, $entryPointId]; + return 'stub'; + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string + { + $this->createAuthenticationSuccessHandler($container, $firewallName, $config); + $this->createAuthenticationFailureHandler($container, $firewallName, $config); + + return 'stub_authenticator_id'; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php deleted file mode 100644 index e5044a2bb92b6..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php +++ /dev/null @@ -1,180 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; -use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; - -class GuardAuthenticationFactoryTest extends TestCase -{ - /** - * @dataProvider getValidConfigurationTests - */ - public function testAddValidConfiguration(array $inputConfig, array $expectedConfig) - { - $factory = new GuardAuthenticationFactory(); - $nodeDefinition = new ArrayNodeDefinition('guard'); - $factory->addConfiguration($nodeDefinition); - - $node = $nodeDefinition->getNode(); - $normalizedConfig = $node->normalize($inputConfig); - $finalizedConfig = $node->finalize($normalizedConfig); - - $this->assertEquals($expectedConfig, $finalizedConfig); - } - - /** - * @dataProvider getInvalidConfigurationTests - */ - public function testAddInvalidConfiguration(array $inputConfig) - { - $this->expectException(InvalidConfigurationException::class); - $factory = new GuardAuthenticationFactory(); - $nodeDefinition = new ArrayNodeDefinition('guard'); - $factory->addConfiguration($nodeDefinition); - - $node = $nodeDefinition->getNode(); - $normalizedConfig = $node->normalize($inputConfig); - // will validate and throw an exception on invalid - $node->finalize($normalizedConfig); - } - - public function getValidConfigurationTests() - { - $tests = []; - - // completely basic - $tests[] = [ - [ - 'authenticators' => ['authenticator1', 'authenticator2'], - 'provider' => 'some_provider', - 'entry_point' => 'the_entry_point', - ], - [ - 'authenticators' => ['authenticator1', 'authenticator2'], - 'provider' => 'some_provider', - 'entry_point' => 'the_entry_point', - ], - ]; - - // testing xml config fix: authenticator -> authenticators - $tests[] = [ - [ - 'authenticator' => ['authenticator1', 'authenticator2'], - ], - [ - 'authenticators' => ['authenticator1', 'authenticator2'], - 'entry_point' => null, - ], - ]; - - return $tests; - } - - public function getInvalidConfigurationTests() - { - $tests = []; - - // testing not empty - $tests[] = [ - ['authenticators' => []], - ]; - - return $tests; - } - - public function testBasicCreate() - { - // simple configuration - $config = [ - 'authenticators' => ['authenticator123'], - 'entry_point' => null, - ]; - [$container, $entryPointId] = $this->executeCreate($config, null); - $this->assertEquals('authenticator123', $entryPointId); - - $providerDefinition = $container->getDefinition('security.authentication.provider.guard.my_firewall'); - $this->assertEquals([ - 'index_0' => new IteratorArgument([new Reference('authenticator123')]), - 'index_1' => new Reference('my_user_provider'), - 'index_2' => 'my_firewall', - 'index_3' => new Reference('security.user_checker.my_firewall'), - ], $providerDefinition->getArguments()); - - $listenerDefinition = $container->getDefinition('security.authentication.listener.guard.my_firewall'); - $this->assertEquals('my_firewall', $listenerDefinition->getArgument(2)); - $this->assertEquals([new Reference('authenticator123')], $listenerDefinition->getArgument(3)->getValues()); - } - - public function testExistingDefaultEntryPointUsed() - { - // any existing default entry point is used - $config = [ - 'authenticators' => ['authenticator123'], - 'entry_point' => null, - ]; - [, $entryPointId] = $this->executeCreate($config, 'some_default_entry_point'); - $this->assertEquals('some_default_entry_point', $entryPointId); - } - - public function testCannotOverrideDefaultEntryPoint() - { - $this->expectException(\LogicException::class); - // any existing default entry point is used - $config = [ - 'authenticators' => ['authenticator123'], - 'entry_point' => 'authenticator123', - ]; - $this->executeCreate($config, 'some_default_entry_point'); - } - - public function testMultipleAuthenticatorsRequiresEntryPoint() - { - $this->expectException(\LogicException::class); - // any existing default entry point is used - $config = [ - 'authenticators' => ['authenticator123', 'authenticatorABC'], - 'entry_point' => null, - ]; - $this->executeCreate($config, null); - } - - public function testCreateWithEntryPoint() - { - // any existing default entry point is used - $config = [ - 'authenticators' => ['authenticator123', 'authenticatorABC'], - 'entry_point' => 'authenticatorABC', - ]; - [, $entryPointId] = $this->executeCreate($config, null); - $this->assertEquals('authenticatorABC', $entryPointId); - } - - private function executeCreate(array $config, $defaultEntryPointId) - { - $container = new ContainerBuilder(); - $container->register('security.authentication.provider.guard'); - $container->register('security.authentication.listener.guard'); - $id = 'my_firewall'; - $userProviderId = 'my_user_provider'; - - $factory = new GuardAuthenticationFactory(); - [, , $entryPointId] = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); - - return [$container, $entryPointId]; - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/LoginLinkFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/LoginLinkFactoryTest.php new file mode 100644 index 0000000000000..8f9450a97d25c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/LoginLinkFactoryTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\LoginLinkFactory; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class LoginLinkFactoryTest extends TestCase +{ + public function testBasicServiceConfiguration() + { + $container = new ContainerBuilder(); + + $config = [ + 'check_route' => 'app_check_login_link', + 'lifetime' => 500, + 'signature_properties' => ['email', 'password'], + 'success_handler' => 'success_handler_service_id', + 'failure_handler' => 'failure_handler_service_id', + ]; + + $factory = new LoginLinkFactory(); + $finalizedConfig = $this->processConfig($config, $factory); + $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider'); + + $this->assertTrue($container->hasDefinition('security.authenticator.login_link')); + $this->assertTrue($container->hasDefinition('security.authenticator.login_link_handler.firewall1')); + } + + private function processConfig(array $config, LoginLinkFactory $factory) + { + $nodeDefinition = new ArrayNodeDefinition('login-link'); + $factory->addConfiguration($nodeDefinition); + + $node = $nodeDefinition->getNode(); + $normalizedConfig = $node->normalize($config); + + return $node->finalize($normalizedConfig); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 9d96cbe36b5b7..1e5f12537f8d1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -12,19 +12,36 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Fixtures\UserProvider\DummyProvider; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\InMemoryUserChecker; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; class SecurityExtensionTest extends TestCase { + use ExpectDeprecationTrait; + public function testInvalidCheckPath() { $this->expectException(InvalidConfigurationException::class); @@ -32,6 +49,7 @@ public function testInvalidCheckPath() $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -49,27 +67,6 @@ public function testInvalidCheckPath() $container->compile(); } - public function testFirewallWithoutAuthenticationListener() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('No authentication listener registered for firewall "some_firewall"'); - $container = $this->getRawContainer(); - - $container->loadFromExtension('security', [ - 'providers' => [ - 'default' => ['id' => 'foo'], - ], - - 'firewalls' => [ - 'some_firewall' => [ - 'pattern' => '/.*', - ], - ], - ]); - - $container->compile(); - } - public function testFirewallWithInvalidUserProvider() { $this->expectException(InvalidConfigurationException::class); @@ -80,6 +77,7 @@ public function testFirewallWithInvalidUserProvider() $extension->addUserProviderFactory(new DummyProvider()); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'my_foo' => ['foo' => []], ], @@ -100,6 +98,7 @@ public function testDisableRoleHierarchyVoter() $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -119,38 +118,12 @@ public function testDisableRoleHierarchyVoter() $this->assertFalse($container->hasDefinition('security.access.role_hierarchy_voter')); } - public function testGuardHandlerIsPassedStatelessFirewalls() - { - $container = $this->getRawContainer(); - - $container->loadFromExtension('security', [ - 'providers' => [ - 'default' => ['id' => 'foo'], - ], - - 'firewalls' => [ - 'some_firewall' => [ - 'pattern' => '^/admin', - 'http_basic' => null, - ], - 'stateless_firewall' => [ - 'pattern' => '/.*', - 'stateless' => true, - 'http_basic' => null, - ], - ], - ]); - - $container->compile(); - $definition = $container->getDefinition('security.authentication.guard_handler'); - $this->assertSame(['stateless_firewall'], $definition->getArgument(2)); - } - public function testSwitchUserNotStatelessOnStatelessFirewall() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -173,6 +146,7 @@ public function testPerListenerProvider() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'first' => ['id' => 'foo'], 'second' => ['id' => 'bar'], @@ -192,9 +166,10 @@ public function testPerListenerProvider() public function testMissingProviderForListener() { $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Not configuring explicitly the provider for the "http_basic" listener on "ambiguous" firewall is ambiguous as there is more than one registered provider.'); + $this->expectExceptionMessage('Not configuring explicitly the provider for the "http_basic" authenticator on "ambiguous" firewall is ambiguous as there is more than one registered provider.'); $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'first' => ['id' => 'foo'], 'second' => ['id' => 'bar'], @@ -215,6 +190,7 @@ public function testPerListenerProviderWithRememberMeAndAnonymous() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'first' => ['id' => 'foo'], 'second' => ['id' => 'bar'], @@ -224,7 +200,6 @@ public function testPerListenerProviderWithRememberMeAndAnonymous() 'default' => [ 'form_login' => ['provider' => 'second'], 'remember_me' => ['secret' => 'baz'], - 'anonymous' => true, ], ], ]); @@ -240,6 +215,7 @@ public function testRegisterRequestMatchersWithAllowIfExpression() $rawExpression = "'foo' == 'bar' or 1 in [1, 3, 3]"; $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -278,6 +254,7 @@ public function testRemovesExpressionCacheWarmerDefinitionIfNoExpressions() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -298,6 +275,7 @@ public function testRegisterTheUserProviderAlias() $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -320,6 +298,7 @@ public function testDoNotRegisterTheUserProviderAliasWithMultipleProviders() $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'first' => ['id' => 'foo'], 'second' => ['id' => 'bar'], @@ -339,36 +318,79 @@ public function testDoNotRegisterTheUserProviderAliasWithMultipleProviders() } /** - * @dataProvider sessionConfigurationProvider + * @group legacy */ - public function testRememberMeCookieInheritFrameworkSessionCookie($config, $samesite, $secure) + public function testFirewallWithNoUserProviderTriggerDeprecation() { $container = $this->getRawContainer(); - $container->registerExtension(new FrameworkExtension()); - $container->setParameter('kernel.bundles_metadata', []); - $container->setParameter('kernel.project_dir', __DIR__); - $container->setParameter('kernel.root_dir', __DIR__); - $container->setParameter('kernel.cache_dir', __DIR__); - $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + + 'providers' => [ + 'first' => ['id' => 'foo'], + 'second' => ['id' => 'foo'], + ], + 'firewalls' => [ - 'default' => [ - 'form_login' => null, - 'remember_me' => ['secret' => 'baz'], + 'some_firewall' => [ + 'custom_authenticator' => 'my_authenticator', ], ], ]); - $container->loadFromExtension('framework', [ - 'session' => $config, + + $this->expectDeprecation('Since symfony/security-bundle 5.4: Not configuring explicitly the provider for the "some_firewall" firewall is deprecated because it\'s ambiguous as there is more than one registered provider. Set the "provider" key to one of the configured providers, even if your custom authenticators don\'t use it.'); + + $container->compile(); + } + + /** + * @dataProvider acceptableIpsProvider + */ + public function testAcceptableAccessControlIps($ips) + { + $container = $this->getRawContainer(); + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + 'firewalls' => [ + 'some_firewall' => [ + 'pattern' => '/.*', + 'http_basic' => [], + ], + ], + 'access_control' => [ + ['ips' => $ips, 'path' => '/somewhere', 'roles' => 'IS_AUTHENTICATED_FULLY'], + ], ]); $container->compile(); - $definition = $container->getDefinition('security.authentication.rememberme.services.simplehash.default'); + $this->assertTrue(true, 'Ip addresses is successfully consumed: '.(\is_string($ips) ? $ips : json_encode($ips))); + } + + public function testCustomRememberMeHandler() + { + $container = $this->getRawContainer(); - $this->assertEquals($samesite, $definition->getArgument(3)['samesite']); - $this->assertEquals($secure, $definition->getArgument(3)['secure']); + $container->register('custom_remember_me', \stdClass::class); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + 'default' => [ + 'remember_me' => ['secret' => 'very', 'service' => 'custom_remember_me'], + ], + ], + ]); + + $container->compile(); + + $handler = $container->getDefinition('security.authenticator.remember_me_handler.default'); + $this->assertEquals(\stdClass::class, $handler->getClass()); + $this->assertEquals([['firewall' => 'default']], $handler->getTag('security.remember_me_handler')); } public function sessionConfigurationProvider() @@ -381,6 +403,7 @@ public function sessionConfigurationProvider() ], [ [ + 'storage_factory_id' => 'session.storage.factory.native', 'cookie_secure' => true, 'cookie_samesite' => 'lax', 'save_path' => null, @@ -391,10 +414,21 @@ public function sessionConfigurationProvider() ]; } + public function acceptableIpsProvider(): iterable + { + yield [['127.0.0.1']]; + yield ['127.0.0.1']; + yield ['127.0.0.1, 127.0.0.2']; + yield ['127.0.0.1/8, 127.0.0.2/16']; + yield [['127.0.0.1/8, 127.0.0.2/16']]; + yield [['127.0.0.1/8', '127.0.0.2/16']]; + } + public function testSwitchUserWithSeveralDefinedProvidersButNoFirewallRootProviderConfigured() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'first' => ['id' => 'foo'], 'second' => ['id' => 'bar'], @@ -405,7 +439,6 @@ public function testSwitchUserWithSeveralDefinedProvidersButNoFirewallRootProvid 'switch_user' => [ 'provider' => 'second', ], - 'anonymous' => true, ], ], ]); @@ -420,6 +453,7 @@ public function testInvalidAccessControlWithEmptyRow() $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -445,6 +479,7 @@ public function testValidAccessControlWithEmptyRow() $container = $this->getRawContainer(); $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, 'providers' => [ 'default' => ['id' => 'foo'], ], @@ -465,6 +500,171 @@ public function testValidAccessControlWithEmptyRow() $this->assertTrue(true, 'extension throws an InvalidConfigurationException if there is one more more empty access control items'); } + /** + * @dataProvider provideEntryPointRequiredData + */ + public function testEntryPointRequired(array $firewall, $messageRegex) + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessageMatches($messageRegex); + + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'first' => ['id' => 'users'], + ], + + 'firewalls' => [ + 'main' => $firewall, + ], + ]); + + $container->compile(); + } + + public function provideEntryPointRequiredData() + { + // more than one entry point available and not explicitly set + yield [ + ['http_basic' => true, 'form_login' => true], + '/Because you have multiple authenticators in firewall "main", you need to set the "entry_point" key to one of your authenticators \("form_login", "http_basic"\) or a service ID implementing/', + ]; + } + + /** + * @dataProvider provideConfigureCustomAuthenticatorData + */ + public function testConfigureCustomAuthenticator(array $firewall, array $expectedAuthenticators) + { + $container = $this->getRawContainer(); + $container->register(TestAuthenticator::class); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'first' => ['id' => 'users'], + ], + + 'firewalls' => [ + 'main' => $firewall, + ], + ]); + + $container->compile(); + + $this->assertEquals($expectedAuthenticators, array_map('strval', $container->getDefinition('security.authenticator.manager.main')->getArgument(0))); + } + + public function provideConfigureCustomAuthenticatorData() + { + yield [ + ['custom_authenticator' => TestAuthenticator::class], + [TestAuthenticator::class], + ]; + + yield [ + ['custom_authenticators' => [TestAuthenticator::class, HttpBasicAuthenticator::class]], + [TestAuthenticator::class, HttpBasicAuthenticator::class], + ]; + } + + public function testCompilesWithoutSessionListenerWithStatelessFirewallWithAuthenticatorManager() + { + $container = $this->getRawContainer(); + + $firewallId = 'stateless_firewall'; + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + $firewallId => [ + 'pattern' => '/.*', + 'stateless' => true, + 'http_basic' => null, + ], + ], + ]); + + $container->compile(); + + $this->assertFalse($container->has('security.listener.session.'.$firewallId)); + } + + public function testCompilesWithSessionListenerWithStatefulllFirewallWithAuthenticatorManager() + { + $container = $this->getRawContainer(); + + $firewallId = 'statefull_firewall'; + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + $firewallId => [ + 'pattern' => '/.*', + 'stateless' => false, + 'http_basic' => null, + ], + ], + ]); + + $container->compile(); + + $this->assertTrue($container->has('security.listener.session.'.$firewallId)); + } + + /** + * @dataProvider provideUserCheckerConfig + */ + public function testUserCheckerWithAuthenticatorManager(array $config, string $expectedUserCheckerClass) + { + $container = $this->getRawContainer(); + $container->register(TestUserChecker::class); + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + 'main' => array_merge([ + 'pattern' => '/.*', + 'http_basic' => true, + ], $config), + ], + ]); + + $container->compile(); + + $userCheckerId = (string) $container->getDefinition('security.listener.user_checker.main')->getArgument(0); + $this->assertTrue($container->has($userCheckerId)); + $this->assertEquals($expectedUserCheckerClass, $container->findDefinition($userCheckerId)->getClass()); + } + + public function provideUserCheckerConfig() + { + yield [[], InMemoryUserChecker::class]; + yield [['user_checker' => TestUserChecker::class], TestUserChecker::class]; + } + + public function testConfigureCustomFirewallListener() + { + $container = $this->getRawContainer(); + /** @var SecurityExtension $extension */ + $extension = $container->getExtension('security'); + $extension->addAuthenticatorFactory(new TestFirewallListenerFactory()); + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + 'main' => [ + 'custom_listener' => true, + ], + ], + ]); + + $container->compile(); + + /** @var IteratorArgument $listenersIteratorArgument */ + $listenersIteratorArgument = $container->getDefinition('security.firewall.map.context.main')->getArgument(0); + $firewallListeners = array_map('strval', $listenersIteratorArgument->getValues()); + $this->assertContains('custom_firewall_listener_id', $firewallListeners); + } + protected function getRawContainer() { $container = new ContainerBuilder(); @@ -473,13 +673,13 @@ protected function getRawContainer() $security = new SecurityExtension(); $container->registerExtension($security); - $bundle = new SecurityBundle(); - $bundle->build($container); - - $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setOptimizationPasses([new ResolveChildDefinitionsPass()]); $container->getCompilerPassConfig()->setRemovingPasses([]); $container->getCompilerPassConfig()->setAfterRemovingPasses([]); + $bundle = new SecurityBundle(); + $bundle->build($container); + return $container; } @@ -491,3 +691,73 @@ protected function getContainer() return $container; } } + +class TestAuthenticator implements AuthenticatorInterface +{ + public function supports(Request $request): ?bool + { + } + + public function authenticate(Request $request): Passport + { + } + + /** + * @internal for compatibility with Symfony 5.4 + */ + public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface + { + } + + public function createToken(Passport $passport, string $firewallName): TokenInterface + { + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + } +} + +class TestUserChecker implements UserCheckerInterface +{ + public function checkPreAuth(UserInterface $user) + { + } + + public function checkPostAuth(UserInterface $user) + { + } +} + +class TestFirewallListenerFactory implements AuthenticatorFactoryInterface, FirewallListenerFactoryInterface +{ + public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array + { + $container->register('custom_firewall_listener_id', \stdClass::class); + + return ['custom_firewall_listener_id']; + } + + public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string + { + return 'test_authenticator_id'; + } + + public function getPriority(): int + { + return 0; + } + + public function getKey(): string + { + return 'custom_listener'; + } + + public function addConfiguration(NodeDefinition $builder) + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Fixtures/TokenInterface.php b/src/Symfony/Bundle/SecurityBundle/Tests/Fixtures/TokenInterface.php deleted file mode 100644 index 1102a4f3b0d16..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Fixtures/TokenInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -isRedirect(), 'Response is not a redirect, got status code: '.substr($response, 0, 2000)); + self::assertTrue($response->isRedirect(), "Response is not a redirect, got:\n".(($p = strpos($response, '-->')) ? substr($response, 0, $p + 3) : $response)); self::assertEquals('http://localhost'.$location, $response->headers->get('Location')); } @@ -63,7 +63,7 @@ protected static function createKernel(array $options = []): KernelInterface $options['test_case'], $options['root_config'] ?? 'config.yml', $options['environment'] ?? strtolower(static::getVarDir().$options['test_case']), - $options['debug'] ?? false + $options['debug'] ?? false, ); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php deleted file mode 100644 index fdee9bce9b06a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional; - -class AnonymousTest extends AbstractWebTestCase -{ - public function testAnonymous() - { - $client = $this->createClient(['test_case' => 'Anonymous', 'root_config' => 'config.yml']); - - $client->request('GET', '/'); - - $this->assertSame(401, $client->getResponse()->getStatusCode()); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php index dcfd6f29e8fea..03b4663ef9250 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php @@ -15,7 +15,7 @@ class AuthenticationCommencingTest extends AbstractWebTestCase { public function testAuthenticationIsCommencingIfAccessDeniedExceptionIsWrapped() { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']); + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'base_config.yml']); $client->request('GET', '/secure-but-not-covered-by-access-control'); $this->assertRedirect($client->getResponse(), '/login'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php new file mode 100644 index 0000000000000..10eeb39ca8c5e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticatorTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +class AuthenticatorTest extends AbstractWebTestCase +{ + /** + * @group legacy + * + * @dataProvider provideEmails + */ + public function testLegacyGlobalUserProvider($email) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'implicit_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } + + /** + * @dataProvider provideEmails + */ + public function testFirewallUserProvider($email, $withinFirewall) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'firewall_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + + if ($withinFirewall) { + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } else { + $this->assertJsonStringEqualsJsonString('{"error":"Invalid credentials."}', $client->getResponse()->getContent()); + } + } + + /** + * @dataProvider provideEmails + */ + public function testWithoutUserProvider($email) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'no_user_provider.yml']); + + $client->request('GET', '/profile', [], [], [ + 'HTTP_X-USER-EMAIL' => $email, + ]); + + $this->assertJsonStringEqualsJsonString('{"email":"'.$email.'"}', $client->getResponse()->getContent()); + } + + public function provideEmails() + { + yield ['jane@example.org', true]; + yield ['john@example.org', false]; + } + + /** + * @dataProvider provideEmailsWithFirewalls + */ + public function testLoginUsersWithMultipleFirewalls(string $username, string $firewallContext) + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'multiple_firewall_user_provider.yml']); + $client->request('GET', '/main/login/check'); + + $client->request('POST', '/'.$firewallContext.'/login/check', [ + '_username' => $username, + '_password' => 'test', + ]); + $this->assertResponseRedirects('/'.$firewallContext.'/user_profile'); + + $client->request('GET', '/'.$firewallContext.'/user_profile'); + $this->assertEquals('Welcome '.$username.'!', $client->getResponse()->getContent()); + } + + public function provideEmailsWithFirewalls() + { + yield ['jane@example.org', 'main']; + yield ['john@example.org', 'custom']; + } + + public function testMultipleFirewalls() + { + $client = $this->createClient(['test_case' => 'Authenticator', 'root_config' => 'multiple_firewalls.yml']); + + $client->request('POST', '/firewall1/login', [ + '_username' => 'jane@example.org', + '_password' => 'test', + ]); + + $client->request('GET', '/firewall2/profile'); + $this->assertResponseRedirects('http://localhost/login'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php index 9d17b13a10b6f..9e3b4a5523783 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php @@ -21,12 +21,12 @@ public function testAccessDecisionManagerAutowiring() { static::bootKernel(['debug' => false]); - $autowiredServices = static::$container->get('test.autowiring_types.autowired_services'); + $autowiredServices = static::getContainer()->get('test.autowiring_types.autowired_services'); $this->assertInstanceOf(AccessDecisionManager::class, $autowiredServices->getAccessDecisionManager(), 'The security.access.decision_manager service should be injected in debug mode'); static::bootKernel(['debug' => true]); - $autowiredServices = static::$container->get('test.autowiring_types.autowired_services'); + $autowiredServices = static::getContainer()->get('test.autowiring_types.autowired_services'); $this->assertInstanceOf(TraceableAccessDecisionManager::class, $autowiredServices->getAccessDecisionManager(), 'The debug.security.access.decision_manager service should be injected in non-debug mode'); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php deleted file mode 100644 index 5069fa9cc7fa9..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; - -class AppCustomAuthenticator extends AbstractGuardAuthenticator -{ - public function supports(Request $request) - { - return false; - } - - public function getCredentials(Request $request) - { - } - - public function getUser($credentials, UserProviderInterface $userProvider) - { - } - - public function checkCredentials($credentials, UserInterface $user) - { - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) - { - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) - { - } - - public function start(Request $request, AuthenticationException $authException = null) - { - return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); - } - - public function supportsRememberMe() - { - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php new file mode 100644 index 0000000000000..f0558c5c5f5a6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ApiAuthenticator.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; + +class ApiAuthenticator extends AbstractAuthenticator +{ + private $selfLoadingUser = false; + + public function __construct(bool $selfLoadingUser = false) + { + $this->selfLoadingUser = $selfLoadingUser; + } + + public function supports(Request $request): ?bool + { + return $request->headers->has('X-USER-EMAIL'); + } + + public function authenticate(Request $request): Passport + { + $email = $request->headers->get('X-USER-EMAIL'); + if (false === strpos($email, '@')) { + throw new BadCredentialsException('Email is not a valid email address.'); + } + + $userLoader = null; + if ($this->selfLoadingUser) { + $userLoader = function ($username) { return new InMemoryUser($username, 'test', ['ROLE_USER']); }; + } + + return new SelfValidatingPassport(new UserBadge($email, $userLoader)); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + return new JsonResponse([ + 'error' => $exception->getMessageKey(), + ], JsonResponse::HTTP_FORBIDDEN); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php new file mode 100644 index 0000000000000..1004ee2c10ba7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/LoginFormAuthenticator.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Util\TargetPathTrait; + +class LoginFormAuthenticator extends AbstractLoginFormAuthenticator +{ + use TargetPathTrait; + + /** @var UrlGeneratorInterface */ + private $urlGenerator; + + public function __construct(UrlGeneratorInterface $urlGenerator) + { + $this->urlGenerator = $urlGenerator; + } + + public function authenticate(Request $request): Passport + { + $username = $request->request->get('_username', ''); + + $request->getSession()->set(Security::LAST_USERNAME, $username); + + return new Passport( + new UserBadge($username), + new PasswordCredentials($request->request->get('_password', '')), + [] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $firewallName): ?Response + { + return new RedirectResponse($this->urlGenerator->generate('security_'.$firewallName.'_profile')); + } + + protected function getLoginUrl(Request $request): string + { + return strpos($request->getUri(), 'main') ? + $this->urlGenerator->generate('security_main_login') : + $this->urlGenerator->generate('security_custom_login') + ; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php new file mode 100644 index 0000000000000..3e1598ea593cd --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/ProfileController.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + +class ProfileController extends AbstractController +{ + public function __invoke() + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + return $this->json(['email' => $this->getUser()->getUserIdentifier()]); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/SecurityController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/SecurityController.php new file mode 100644 index 0000000000000..04d165b015e59 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AuthenticatorBundle/SecurityController.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; + +class SecurityController extends AbstractController +{ + public function checkAction() + { + return new Response('OK'); + } + + public function profileAction() + { + return new Response('Welcome '.$this->getUser()->getUserIdentifier().'!'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php index bafa0f68ce33d..c77b1e204e0db 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Controller/LoginController.php @@ -11,14 +11,21 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Psr\Container\ContainerInterface; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; -class LoginController implements ContainerAwareInterface +class LoginController implements ServiceSubscriberInterface { - use ContainerAwareTrait; + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } public function loginAction() { @@ -43,4 +50,15 @@ public function secureAction() { throw new \Exception('Wrapper', 0, new \Exception('Another Wrapper', 0, new AccessDeniedException())); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices(): array + { + return [ + 'form.factory' => FormFactoryInterface::class, + 'twig' => Environment::class, + ]; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig index a117cb94f8778..9a9bfbc731397 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig @@ -1,7 +1,7 @@ {% extends "base.html.twig" %} {% block body %} - Hello {{ app.user.username }}!

+ Hello {{ app.user.userIdentifier }}!

You're browsing to path "{{ app.request.pathInfo }}".

Log out. Log out. diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php index cf0e1150aff9a..11d00e257e98a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php @@ -11,15 +11,21 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; -class LocalizedController implements ContainerAwareInterface +class LocalizedController implements ServiceSubscriberInterface { - use ContainerAwareTrait; + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } public function loginAction(Request $request) { @@ -61,4 +67,14 @@ public function homepageAction() { return (new Response('Homepage'))->setPublic(); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices(): array + { + return [ + 'twig' => Environment::class, + ]; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php index 60eef86718775..db6aacca8cfc2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LoginController.php @@ -11,17 +11,23 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; -class LoginController implements ContainerAwareInterface +class LoginController implements ServiceSubscriberInterface { - use ContainerAwareTrait; + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } public function loginAction(Request $request, UserInterface $user = null) { @@ -53,4 +59,14 @@ public function secureAction() { throw new \Exception('Wrapper', 0, new \Exception('Another Wrapper', 0, new AccessDeniedException())); } + + /** + * {@inheritdoc} + */ + public static function getSubscribedServices(): array + { + return [ + 'twig' => Environment::class, + ]; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php index eab1913ec184d..c330723adff15 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/FormLoginBundle.php @@ -11,8 +11,28 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LocalizedController; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\Controller\LoginController; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class FormLoginBundle extends Bundle { + /** + * {@inheritdoc} + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container + ->register(LoginController::class) + ->setPublic(true) + ->addTag('container.service_subscriber'); + + $container + ->register(LocalizedController::class) + ->setPublic(true) + ->addTag('container.service_subscriber'); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig index 3f88aae903536..fd51df2a4383f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/after_login.html.twig @@ -1,7 +1,7 @@ {% extends "base.html.twig" %} {% block body %} - Hello {{ user.username }}!

+ Hello {{ user.userIdentifier }}!

You're browsing to path "{{ app.request.pathInfo }}". Log out. diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig index 059f5f2bca1d2..34ea19f2bde62 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/views/Login/login.html.twig @@ -3,7 +3,8 @@ {% block body %} {% if error %} -
{{ error.message }}
+
{{ error.messageKey }}
+
{{ error.messageKey|replace(error.messageData) }}
{% endif %} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php deleted file mode 100644 index 22d378835e4c0..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; -use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; - -class AppCustomAuthenticator extends AbstractGuardAuthenticator -{ - public function supports(Request $request) - { - return '/manual_login' !== $request->getPathInfo() && '/profile' !== $request->getPathInfo(); - } - - public function getCredentials(Request $request) - { - throw new AuthenticationException('This should be hit'); - } - - public function getUser($credentials, UserProviderInterface $userProvider) - { - } - - public function checkCredentials($credentials, UserInterface $user) - { - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) - { - return new Response('', 418); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) - { - } - - public function start(Request $request, AuthenticationException $authException = null) - { - return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); - } - - public function supportsRememberMe() - { - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AuthenticationController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AuthenticationController.php deleted file mode 100644 index 9833d05513833..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AuthenticationController.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; -use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; - -class AuthenticationController -{ - public function manualLoginAction(GuardAuthenticatorHandler $guardAuthenticatorHandler, Request $request) - { - $guardAuthenticatorHandler->authenticateWithToken(new PostAuthenticationGuardToken(new User('Jane', 'test', ['ROLE_USER']), 'secure', ['ROLE_USER']), $request, 'secure'); - - return new Response('Logged in.'); - } - - public function profileAction(UserInterface $user = null) - { - if (null === $user) { - return new Response('Not logged in.'); - } - - return new Response('Username: '.$user->getUsername()); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Controller/TestController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Controller/TestController.php index cba75a1526ace..6bd571d15e217 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Controller/TestController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Controller/TestController.php @@ -21,6 +21,6 @@ class TestController { public function loginCheckAction(UserInterface $user) { - return new JsonResponse(['message' => sprintf('Welcome @%s!', $user->getUsername())]); + return new JsonResponse(['message' => sprintf('Welcome @%s!', $user->getUserIdentifier())]); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php index a0300d4d78387..4aabaacd4889c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php @@ -21,6 +21,6 @@ class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandlerIn { public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { - return new JsonResponse(['message' => sprintf('Good game @%s!', $token->getUsername())]); + return new JsonResponse(['message' => sprintf('Good game @%s!', $token->getUserIdentifier())]); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php new file mode 100644 index 0000000000000..0d1501508b58a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/LoginLink/TestCustomLoginLinkSuccessHandler.php @@ -0,0 +1,17 @@ + sprintf('Welcome %s!', $token->getUserIdentifier())]); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Controller/ProfileController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Controller/ProfileController.php new file mode 100644 index 0000000000000..7f99d17c90123 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Controller/ProfileController.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\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Controller; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\User\UserInterface; + +class ProfileController +{ + public function __invoke(UserInterface $user) + { + return new Response($user->getUserIdentifier()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/RememberMeBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/RememberMeBundle.php new file mode 100644 index 0000000000000..191af0057e468 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/RememberMeBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class RememberMeBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php new file mode 100644 index 0000000000000..a51702eec15b6 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/StaticTokenProvider.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security; + +use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface; +use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface; +use Symfony\Component\Security\Core\Exception\TokenNotFoundException; + +class StaticTokenProvider implements TokenProviderInterface +{ + private static $db = []; + private static $kernelClass; + + public function __construct($kernel) + { + // only reset the "internal db" for new tests + if (self::$kernelClass !== \get_class($kernel)) { + self::$kernelClass = \get_class($kernel); + self::$db = []; + } + } + + public function loadTokenBySeries(string $series): PersistentTokenInterface + { + $token = self::$db[$series] ?? false; + if (!$token) { + throw new TokenNotFoundException(); + } + + return $token; + } + + public function deleteTokenBySeries(string $series) + { + unset(self::$db[$series]); + } + + public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed) + { + $token = $this->loadTokenBySeries($series); + $refl = new \ReflectionClass($token); + $tokenValueProp = $refl->getProperty('tokenValue'); + $tokenValueProp->setAccessible(true); + $tokenValueProp->setValue($token, $tokenValue); + + $lastUsedProp = $refl->getProperty('lastUsed'); + $lastUsedProp->setAccessible(true); + $lastUsedProp->setValue($token, $lastUsed); + + self::$db[$series] = $token; + } + + public function createNewToken(PersistentTokenInterface $token) + { + self::$db[$token->getSeries()] = $token; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php new file mode 100644 index 0000000000000..c40a7e0a12267 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security; + +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +class UserChangingUserProvider implements UserProviderInterface +{ + private $inner; + + public static $changePassword = false; + + public function __construct(InMemoryUserProvider $inner) + { + $this->inner = $inner; + } + + public function loadUserByUsername($username): UserInterface + { + return $this->changeUser($this->inner->loadUserByUsername($username)); + } + + public function loadUserByIdentifier(string $userIdentifier): UserInterface + { + return $this->changeUser($this->inner->loadUserByIdentifier($userIdentifier)); + } + + public function refreshUser(UserInterface $user): UserInterface + { + return $this->changeUser($this->inner->refreshUser($user)); + } + + public function supportsClass($class): bool + { + return $this->inner->supportsClass($class); + } + + private function changeUser(UserInterface $user): UserInterface + { + if (self::$changePassword) { + $alterUser = \Closure::bind(function (InMemoryUser $user) { $user->password = 'changed!'; }, null, class_exists(User::class) ? User::class : InMemoryUser::class); + $alterUser($user); + } + + return $user; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/DependencyInjection/RequestTrackerExtension.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/DependencyInjection/RequestTrackerExtension.php new file mode 100644 index 0000000000000..69deb865c2436 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/DependencyInjection/RequestTrackerExtension.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\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\DependencyInjection; + +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\EventSubscriber\RequestTrackerSubscriber; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; + +final class RequestTrackerExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container): void + { + $container->register('request_tracker_subscriber', RequestTrackerSubscriber::class) + ->setPublic(true) + ->addTag('kernel.event_subscriber'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/EventSubscriber/RequestTrackerSubscriber.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/EventSubscriber/RequestTrackerSubscriber.php new file mode 100644 index 0000000000000..9f442aeb11710 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/EventSubscriber/RequestTrackerSubscriber.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +final class RequestTrackerSubscriber implements EventSubscriberInterface +{ + private $lastRequest; + + public static function getSubscribedEvents(): array + { + return [ + RequestEvent::class => 'onRequest', + ]; + } + + public function onRequest(RequestEvent $event) + { + $this->lastRequest = $event->getRequest(); + } + + public function getLastRequest(): ?Request + { + return $this->lastRequest; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/RequestTrackerBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/RequestTrackerBundle.php new file mode 100644 index 0000000000000..0497be83c316c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RequestTrackerBundle/RequestTrackerBundle.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +final class RequestTrackerBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php index fe12ea90f1469..db9d39e7d6e74 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php @@ -4,8 +4,8 @@ use Symfony\Bundle\SecurityBundle\Tests\Functional\UserWithoutEquatable; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -16,7 +16,7 @@ class ArrayUserProvider implements UserProviderInterface public function addUser(UserInterface $user) { - $this->users[$user->getUsername()] = $user; + $this->users[$user->getUserIdentifier()] = $user; } public function setUser($username, UserInterface $user) @@ -29,13 +29,18 @@ public function getUser($username) return $this->users[$username]; } - public function loadUserByUsername($username) + public function loadUserByUsername($username): UserInterface { - $user = $this->getUser($username); + return $this->loadUserByIdentifier($username); + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + $user = $this->getUser($identifier); if (null === $user) { - $e = new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); - $e->setUsername($username); + $e = new UserNotFoundException(sprintf('User "%s" not found.', $identifier)); + $e->setUsername($identifier); throw $e; } @@ -43,20 +48,20 @@ public function loadUserByUsername($username) return $user; } - public function refreshUser(UserInterface $user) + public function refreshUser(UserInterface $user): UserInterface { if (!$user instanceof UserInterface) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } - $storedUser = $this->getUser($user->getUsername()); + $storedUser = $this->getUser($user->getUserIdentifier()); $class = \get_class($storedUser); - return new $class($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $storedUser->isAccountNonExpired(), $storedUser->isCredentialsNonExpired() && $storedUser->getPassword() === $user->getPassword(), $storedUser->isAccountNonLocked()); + return new $class($storedUser->getUserIdentifier(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled()); } - public function supportsClass($class) + public function supportsClass($class): bool { - return User::class === $class || UserWithoutEquatable::class === $class; + return InMemoryUser::class === $class || UserWithoutEquatable::class === $class; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php index 5197a16195e2e..8336dce245792 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -25,14 +24,6 @@ public function build(ContainerBuilder $container) $container->setParameter('container.build_time', time()); $container->setParameter('container.build_id', 'test_bundle'); - $container->addCompilerPass(new class() implements CompilerPassInterface { - public function process(ContainerBuilder $container) - { - $container->removeDefinition('twig.controller.exception'); - $container->removeDefinition('twig.controller.preview_error'); - } - }); - $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php deleted file mode 100644 index 51f56c220d33c..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional; - -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\User\InMemoryUserProvider; -use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; - -class ClearRememberMeTest extends AbstractWebTestCase -{ - public function testUserChangeClearsCookie() - { - $client = $this->createClient(['test_case' => 'ClearRememberMe', 'root_config' => 'config.yml']); - - $client->request('POST', '/login', [ - '_username' => 'johannes', - '_password' => 'test', - ]); - - $this->assertSame(302, $client->getResponse()->getStatusCode()); - $cookieJar = $client->getCookieJar(); - $this->assertNotNull($cookieJar->get('REMEMBERME')); - - $client->request('GET', '/foo'); - $this->assertRedirect($client->getResponse(), '/login'); - $this->assertNull($cookieJar->get('REMEMBERME')); - } -} - -class RememberMeFooController -{ - public function __invoke(UserInterface $user) - { - return new Response($user->getUsername()); - } -} - -class RememberMeUserProvider implements UserProviderInterface -{ - private $inner; - - public function __construct(InMemoryUserProvider $inner) - { - $this->inner = $inner; - } - - public function loadUserByUsername($username) - { - return $this->inner->loadUserByUsername($username); - } - - public function refreshUser(UserInterface $user) - { - $user = $this->inner->refreshUser($user); - - $alterUser = \Closure::bind(function (User $user) { $user->password = 'foo'; }, null, User::class); - $alterUser($user); - - return $user; - } - - public function supportsClass($class) - { - return $this->inner->supportsClass($class); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php index 1a672d70f8335..ad2fc0c63d1e0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php @@ -14,11 +14,11 @@ class CsrfFormLoginTest extends AbstractWebTestCase { /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLoginAndLogoutWithCsrfTokens($config) + public function testFormLoginAndLogoutWithCsrfTokens($options) { - $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['user_login[username]'] = 'johannes'; @@ -43,11 +43,11 @@ public function testFormLoginAndLogoutWithCsrfTokens($config) } /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLoginWithInvalidCsrfToken($config) + public function testFormLoginWithInvalidCsrfToken($options) { - $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['user_login[_token]'] = ''; @@ -60,11 +60,11 @@ public function testFormLoginWithInvalidCsrfToken($config) } /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLoginWithCustomTargetPath($config) + public function testFormLoginWithCustomTargetPath($options) { - $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['user_login[username]'] = 'johannes'; @@ -80,11 +80,11 @@ public function testFormLoginWithCustomTargetPath($config) } /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLoginRedirectsToProtectedResourceAfterLogin($config) + public function testFormLoginRedirectsToProtectedResourceAfterLogin($options) { - $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $client->request('GET', '/protected-resource'); $this->assertRedirect($client->getResponse(), '/login'); @@ -100,11 +100,9 @@ public function testFormLoginRedirectsToProtectedResourceAfterLogin($config) $this->assertStringContainsString('You\'re browsing to path "/protected-resource".', $text); } - public function getConfigs() + public function provideClientOptions() { - return [ - ['config.yml'], - ['routes_as_path.yml'], - ]; + yield [['test_case' => 'CsrfFormLogin', 'root_config' => 'config.yml']]; + yield [['test_case' => 'CsrfFormLogin', 'root_config' => 'routes_as_path.yml']]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php index d1f8860e4b1eb..6b1a54caa0100 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php @@ -11,12 +11,10 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -use Symfony\Bundle\SecurityBundle\Tests\Fixtures\TokenInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\AuthenticationEvents; -use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; -use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\SwitchUserEvent; @@ -31,14 +29,12 @@ public function testAliasedEvents() $dispatcher = $container->get('event_dispatcher'); $dispatcher->dispatch(new AuthenticationSuccessEvent($this->createMock(TokenInterface::class)), AuthenticationEvents::AUTHENTICATION_SUCCESS); - $dispatcher->dispatch(new AuthenticationFailureEvent($this->createMock(TokenInterface::class), new AuthenticationException()), AuthenticationEvents::AUTHENTICATION_FAILURE); $dispatcher->dispatch(new InteractiveLoginEvent($this->createMock(Request::class), $this->createMock(TokenInterface::class)), SecurityEvents::INTERACTIVE_LOGIN); $dispatcher->dispatch(new SwitchUserEvent($this->createMock(Request::class), $this->createMock(UserInterface::class), $this->createMock(TokenInterface::class)), SecurityEvents::SWITCH_USER); $this->assertEquals( [ 'onAuthenticationSuccess' => 1, - 'onAuthenticationFailure' => 1, 'onInteractiveLogin' => 1, 'onSwitchUser' => 1, ], diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php index 77011409cfaa4..7b6f4fb249135 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php @@ -15,22 +15,6 @@ class FirewallEntryPointTest extends AbstractWebTestCase { - public function testItUsesTheConfiguredEntryPointWhenUsingUnknownCredentials() - { - $client = $this->createClient(['test_case' => 'FirewallEntryPoint']); - - $client->request('GET', '/secure/resource', [], [], [ - 'PHP_AUTH_USER' => 'unknown', - 'PHP_AUTH_PW' => 'credentials', - ]); - - $this->assertEquals( - EntryPointStub::RESPONSE_TEXT, - $client->getResponse()->getContent(), - "Custom entry point wasn't started" - ); - } - public function testItUsesTheConfiguredEntryPointFromTheExceptionListenerWithFormLoginAndNoCredentials() { $client = $this->createClient(['test_case' => 'FirewallEntryPoint', 'root_config' => 'config_form_login.yml']); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php index 641ef0e519a1d..0af3fc2cd6c6b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php @@ -14,11 +14,11 @@ class FormLoginTest extends AbstractWebTestCase { /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLogin($config) + public function testFormLogin(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['_username'] = 'johannes'; @@ -33,11 +33,11 @@ public function testFormLogin($config) } /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLogout($config) + public function testFormLogout(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['_username'] = 'johannes'; @@ -66,11 +66,11 @@ public function testFormLogout($config) } /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLoginWithCustomTargetPath($config) + public function testFormLoginWithCustomTargetPath(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['_username'] = 'johannes'; @@ -86,11 +86,11 @@ public function testFormLoginWithCustomTargetPath($config) } /** - * @dataProvider getConfigs + * @dataProvider provideClientOptions */ - public function testFormLoginRedirectsToProtectedResourceAfterLogin($config) + public function testFormLoginRedirectsToProtectedResourceAfterLogin(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $client->request('GET', '/protected_resource'); $this->assertRedirect($client->getResponse(), '/login'); @@ -106,11 +106,50 @@ public function testFormLoginRedirectsToProtectedResourceAfterLogin($config) $this->assertStringContainsString('You\'re browsing to path "/protected_resource".', $text); } - public function getConfigs() + /** + * @group time-sensitive + */ + public function testLoginThrottling() { - return [ - ['config.yml'], - ['routes_as_path.yml'], + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'login_throttling.yml']); + + $attempts = [ + ['johannes', 'wrong'], + ['johannes', 'also_wrong'], + ['wrong', 'wrong'], + ['johannes', 'wrong_again'], ]; + foreach ($attempts as $i => $attempt) { + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = $attempt[0]; + $form['_password'] = $attempt[1]; + $client->submit($form); + + $text = $client->followRedirect()->text(null, true); + switch ($i) { + case 0: // First attempt : Invalid credentials (OK) + $this->assertStringContainsString('Invalid credentials', $text, 'Invalid response on 1st attempt'); + + break; + case 1: // Second attempt : login throttling ! + $this->assertStringContainsString('Too many failed login attempts, please try again in 8 minutes.', $text, 'Invalid response on 2nd attempt'); + + break; + case 2: // Third attempt with unexisting username + $this->assertStringContainsString('Invalid credentials.', $text, 'Invalid response on 3rd attempt'); + + break; + case 3: // Fourth attempt : still login throttling ! + $this->assertStringContainsString('Too many failed login attempts, please try again in 8 minutes.', $text, 'Invalid response on 4th attempt'); + + break; + } + } + } + + public function provideClientOptions() + { + yield [['test_case' => 'StandardFormLogin', 'root_config' => 'base_config.yml']]; + yield [['test_case' => 'StandardFormLogin', 'root_config' => 'routes_as_path.yml']]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php deleted file mode 100644 index 83cd4118d76e4..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional; - -class GuardedTest extends AbstractWebTestCase -{ - public function testGuarded() - { - $client = $this->createClient(['test_case' => 'Guarded', 'root_config' => 'config.yml']); - - $client->request('GET', '/'); - - $this->assertSame(418, $client->getResponse()->getStatusCode()); - } - - public function testManualLogin() - { - $client = $this->createClient(['debug' => true, 'test_case' => 'Guarded', 'root_config' => 'config.yml']); - - $client->request('GET', '/manual_login'); - $client->request('GET', '/profile'); - - $this->assertSame('Username: Jane', $client->getResponse()->getContent()); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php index a69f5e591d1fa..5828a0c3b1b9b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php @@ -13,9 +13,6 @@ use Symfony\Component\HttpFoundation\JsonResponse; -/** - * @author Kévin Dunglas - */ class JsonLoginTest extends AbstractWebTestCase { public function testDefaultJsonLoginSuccess() @@ -61,15 +58,4 @@ public function testCustomJsonLoginFailure() $this->assertSame(500, $response->getStatusCode()); $this->assertSame(['message' => 'Something went wrong'], json_decode($response->getContent(), true)); } - - public function testDefaultJsonLoginBadRequest() - { - $client = $this->createClient(['test_case' => 'JsonLogin', 'root_config' => 'config.yml']); - $client->request('POST', '/chk', [], [], ['CONTENT_TYPE' => 'application/json'], 'Not a json content'); - $response = $client->getResponse(); - - $this->assertSame(400, $response->getStatusCode()); - $this->assertSame('application/json', $response->headers->get('Content-Type')); - $this->assertSame(['type' => 'https://tools.ietf.org/html/rfc2616#section-10', 'title' => 'An error occurred', 'status' => 400, 'detail' => 'Bad Request'], json_decode($response->getContent(), true)); - } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php index 2a45fc00de38f..0c356662c39a7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php @@ -16,7 +16,7 @@ class LocalizedRoutesAsPathTest extends AbstractWebTestCase /** * @dataProvider getLocales */ - public function testLoginLogoutProcedure($locale) + public function testLoginLogoutProcedure(string $locale) { $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'localized_routes.yml']); @@ -35,9 +35,10 @@ public function testLoginLogoutProcedure($locale) } /** + * @group issue-32995 * @dataProvider getLocales */ - public function testLoginFailureWithLocalizedFailurePath($locale) + public function testLoginFailureWithLocalizedFailurePath(string $locale) { $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'localized_form_failure_handler.yml']); @@ -53,7 +54,7 @@ public function testLoginFailureWithLocalizedFailurePath($locale) /** * @dataProvider getLocales */ - public function testAccessRestrictedResource($locale) + public function testAccessRestrictedResource(string $locale) { $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'localized_routes.yml']); @@ -64,7 +65,7 @@ public function testAccessRestrictedResource($locale) /** * @dataProvider getLocales */ - public function testAccessRestrictedResourceWithForward($locale) + public function testAccessRestrictedResourceWithForward(string $locale) { $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'localized_routes_with_forward.yml']); @@ -74,6 +75,7 @@ public function testAccessRestrictedResourceWithForward($locale) public function getLocales() { - return [['en'], ['de']]; + yield ['en']; + yield ['de']; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LoginLinkAuthenticationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LoginLinkAuthenticationTest.php new file mode 100644 index 0000000000000..1457d20e303e2 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LoginLinkAuthenticationTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +/** + * @author Ryan Weaver + */ +class LoginLinkAuthenticationTest extends AbstractWebTestCase +{ + public function testLoginLinkSuccess() + { + $client = $this->createClient(['test_case' => 'LoginLink', 'root_config' => 'config.yml', 'debug' => true]); + + // we need an active request that is under the firewall to use the linker + $request = Request::create('/get-login-link'); + self::getContainer()->get(RequestStack::class)->push($request); + + /** @var LoginLinkHandlerInterface $loginLinkHandler */ + $loginLinkHandler = self::getContainer()->get(LoginLinkHandlerInterface::class); + $user = new InMemoryUser('weaverryan', 'foo'); + $loginLink = $loginLinkHandler->createLoginLink($user); + $this->assertStringContainsString('user=weaverryan', $loginLink); + $this->assertStringContainsString('hash=', $loginLink); + $this->assertStringContainsString('expires=', $loginLink); + $client->request('GET', $loginLink->getUrl()); + $response = $client->getResponse(); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(['message' => 'Welcome weaverryan!'], json_decode($response->getContent(), true)); + + $client->request('GET', $loginLink->getUrl()); + $response = $client->getResponse(); + $this->assertSame(200, $response->getStatusCode()); + + $client->request('GET', $loginLink->getUrl()); + $response = $client->getResponse(); + $this->assertSame(302, $response->getStatusCode(), 'Should redirect with an error because max uses are only 2'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php index cb7868f3256ef..5da52d9602a49 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php @@ -11,53 +11,79 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Component\BrowserKit\Cookie; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\KernelEvents; + class LogoutTest extends AbstractWebTestCase { - public function testSessionLessRememberMeLogout() + public function testCsrfTokensAreClearedOnLogout() { - $client = $this->createClient(['test_case' => 'RememberMeLogout', 'root_config' => 'config.yml']); + $client = $this->createClient(['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']); + $client->disableReboot(); + $this->callInRequestContext($client, function () { + static::getContainer()->get('security.csrf.token_storage')->setToken('foo', 'bar'); + }); $client->request('POST', '/login', [ '_username' => 'johannes', '_password' => 'test', ]); - $cookieJar = $client->getCookieJar(); - $cookieJar->expire(session_name()); - - $this->assertNotNull($cookieJar->get('REMEMBERME')); - $this->assertSame('lax', $cookieJar->get('REMEMBERME')->getSameSite()); + $this->callInRequestContext($client, function () { + $this->assertTrue(static::getContainer()->get('security.csrf.token_storage')->hasToken('foo')); + $this->assertSame('bar', static::getContainer()->get('security.csrf.token_storage')->getToken('foo')); + }); $client->request('GET', '/logout'); - $this->assertNull($cookieJar->get('REMEMBERME')); + $this->callInRequestContext($client, function () { + $this->assertFalse(static::getContainer()->get('security.csrf.token_storage')->hasToken('foo')); + }); } - public function testCsrfTokensAreClearedOnLogout() + public function testAccessControlDoesNotApplyOnLogout() { - $client = $this->createClient(['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']); - static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar'); - - $client->request('POST', '/login', [ - '_username' => 'johannes', - '_password' => 'test', - ]); - - $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo')); - $this->assertSame('bar', static::$container->get('security.csrf.token_storage')->getToken('foo')); + $client = $this->createClient(['test_case' => 'Logout', 'root_config' => 'config_access.yml']); + $client->request('POST', '/login', ['_username' => 'johannes', '_password' => 'test']); $client->request('GET', '/logout'); - $this->assertFalse(static::$container->get('security.csrf.token_storage')->hasToken('foo')); + $this->assertRedirect($client->getResponse(), '/'); } - public function testAccessControlDoesNotApplyOnLogout() + public function testCookieClearingOnLogout() { - $client = $this->createClient(['test_case' => 'LogoutAccess', 'root_config' => 'config.yml']); + $client = $this->createClient(['test_case' => 'Logout', 'root_config' => 'config_cookie_clearing.yml']); + + $cookieJar = $client->getCookieJar(); + $cookieJar->set(new Cookie('flavor', 'chocolate', strtotime('+1 day'), null, 'somedomain')); $client->request('POST', '/login', ['_username' => 'johannes', '_password' => 'test']); $client->request('GET', '/logout'); $this->assertRedirect($client->getResponse(), '/'); + $this->assertNull($cookieJar->get('flavor')); + } + + private function callInRequestContext(KernelBrowser $client, callable $callable): void + { + /** @var EventDispatcherInterface $eventDispatcher */ + $eventDispatcher = static::getContainer()->get(EventDispatcherInterface::class); + $wrappedCallable = function (RequestEvent $event) use (&$callable) { + $callable(); + $event->setResponse(new Response('')); + $event->stopPropagation(); + }; + + $eventDispatcher->addListener(KernelEvents::REQUEST, $wrappedCallable); + try { + $client->request('GET', '/'.uniqid('', true)); + } finally { + $eventDispatcher->removeListener(KernelEvents::REQUEST, $wrappedCallable); + } } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php index 6c8ba6482e45b..a5029c954189a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php @@ -11,20 +11,20 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; + class MissingUserProviderTest extends AbstractWebTestCase { public function testUserProviderIsNeeded() { - $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml', 'debug' => true]); + $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml']); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('"default" firewall requires a user provider but none was defined'); $client->request('GET', '/', [], [], [ 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word', ]); - - $response = $client->getResponse(); - $this->assertSame(500, $response->getStatusCode()); - $this->assertStringContainsString('Symfony\Component\Config\Definition\Exception\InvalidConfigurationException', $response->getContent()); - $this->assertStringContainsString('"default" firewall requires a user provider but none was defined', html_entity_decode($response->getContent())); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php index 6bfa1ed438732..aae5f9282e546 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php @@ -19,8 +19,7 @@ public function testSessionRememberMeSecureCookieFlagAuto($https, $expectedSecur ]); $cookies = $client->getResponse()->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY); - - $this->assertEquals($expectedSecureFlag, $cookies['']['/']['REMEMBERME']->isSecure()); + $this->assertSame($expectedSecureFlag, $cookies['']['/']['REMEMBERME']->isSecure()); } public function getSessionRememberMeSecureCookieFlagAutoHttpsMap() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeTest.php new file mode 100644 index 0000000000000..0ccc17d9c989d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security\UserChangingUserProvider; + +class RememberMeTest extends AbstractWebTestCase +{ + protected function setUp(): void + { + UserChangingUserProvider::$changePassword = false; + } + + /** + * @dataProvider provideConfigs + */ + public function testRememberMe(array $options) + { + $client = $this->createClient(array_merge_recursive(['root_config' => 'config.yml', 'test_case' => 'RememberMe'], $options)); + $client->request('POST', '/login', [ + '_username' => 'johannes', + '_password' => 'test', + ]); + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $client->request('GET', '/profile'); + $this->assertSame('johannes', $client->getResponse()->getContent()); + + // clear session, this should trigger remember me on the next request + $client->getCookieJar()->expire('MOCKSESSID'); + + $client->request('GET', '/profile'); + $this->assertSame('johannes', $client->getResponse()->getContent(), 'Not logged in after resetting session.'); + + // logout, this should clear the remember-me cookie + $client->request('GET', '/logout'); + $this->assertSame(302, $client->getResponse()->getStatusCode(), 'Logout unsuccessful.'); + $this->assertNull($client->getCookieJar()->get('REMEMBERME')); + } + + public function testUserChangeClearsCookie() + { + $client = $this->createClient(['test_case' => 'RememberMe', 'root_config' => 'clear_on_change_config.yml']); + + $client->request('POST', '/login', [ + '_username' => 'johannes', + '_password' => 'test', + ]); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $cookieJar = $client->getCookieJar(); + $this->assertNotNull($cookie = $cookieJar->get('REMEMBERME')); + + UserChangingUserProvider::$changePassword = true; + + // change password (through user provider), this deauthenticates the session + $client->request('GET', '/profile'); + $this->assertRedirect($client->getResponse(), '/login'); + $this->assertNull($cookieJar->get('REMEMBERME')); + + // restore the old remember me cookie, it should no longer be valid + $cookieJar->set($cookie); + $client->request('GET', '/profile'); + $this->assertRedirect($client->getResponse(), '/login'); + } + + public function testSessionLessRememberMeLogout() + { + $client = $this->createClient(['test_case' => 'RememberMe', 'root_config' => 'stateless_config.yml']); + + $client->request('POST', '/login', [ + '_username' => 'johannes', + '_password' => 'test', + ]); + + $cookieJar = $client->getCookieJar(); + $cookieJar->expire(session_name()); + + $this->assertNotNull($cookieJar->get('REMEMBERME')); + $this->assertSame('lax', $cookieJar->get('REMEMBERME')->getSameSite()); + + $client->request('GET', '/logout'); + $this->assertSame(302, $client->getResponse()->getStatusCode(), 'Logout unsuccessful.'); + $this->assertNull($cookieJar->get('REMEMBERME')); + } + + public function provideConfigs() + { + yield [['root_config' => 'config_session.yml']]; + yield [['root_config' => 'config_persistent.yml']]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php index 0303f1b4eeff9..7c1f3dc0679a4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -14,33 +14,33 @@ class SecurityRoutingIntegrationTest extends AbstractWebTestCase { /** - * @dataProvider getConfigs + * @dataProvider provideConfigs */ - public function testRoutingErrorIsNotExposedForProtectedResourceWhenAnonymous($config) + public function testRoutingErrorIsNotExposedForProtectedResourceWhenAnonymous(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $client->request('GET', '/protected_resource'); $this->assertRedirect($client->getResponse(), '/login'); } /** - * @dataProvider getConfigs + * @dataProvider provideConfigs */ - public function testRoutingErrorIsExposedWhenNotProtected($config) + public function testRoutingErrorIsExposedWhenNotProtected(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $client->request('GET', '/unprotected_resource'); $this->assertEquals(404, $client->getResponse()->getStatusCode(), (string) $client->getResponse()); } /** - * @dataProvider getConfigs + * @dataProvider provideConfigs */ - public function testRoutingErrorIsNotExposedForProtectedResourceWhenLoggedInWithInsufficientRights($config) + public function testRoutingErrorIsNotExposedForProtectedResourceWhenLoggedInWithInsufficientRights(array $options) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config]); + $client = $this->createClient($options); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['_username'] = 'johannes'; @@ -53,38 +53,38 @@ public function testRoutingErrorIsNotExposedForProtectedResourceWhenLoggedInWith } /** - * @dataProvider getConfigs + * @dataProvider provideConfigs */ - public function testSecurityConfigurationForSingleIPAddress($config) + public function testSecurityConfigurationForSingleIPAddress(array $options) { - $allowedClient = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['REMOTE_ADDR' => '10.10.10.10']); + $allowedClient = $this->createClient($options, ['REMOTE_ADDR' => '10.10.10.10']); $this->ensureKernelShutdown(); - $barredClient = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['REMOTE_ADDR' => '10.10.20.10']); + $barredClient = $this->createClient($options, ['REMOTE_ADDR' => '10.10.20.10']); $this->assertAllowed($allowedClient, '/secured-by-one-ip'); $this->assertRestricted($barredClient, '/secured-by-one-ip'); } /** - * @dataProvider getConfigs + * @dataProvider provideConfigs */ - public function testSecurityConfigurationForMultipleIPAddresses($config) + public function testSecurityConfigurationForMultipleIPAddresses(array $options) { - $allowedClientA = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['REMOTE_ADDR' => '1.1.1.1']); + $allowedClientA = $this->createClient($options, ['REMOTE_ADDR' => '1.1.1.1']); $this->ensureKernelShutdown(); - $allowedClientB = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['REMOTE_ADDR' => '2.2.2.2']); + $allowedClientB = $this->createClient($options, ['REMOTE_ADDR' => '2.2.2.2']); $this->ensureKernelShutdown(); - $allowedClientC = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['REMOTE_ADDR' => '203.0.113.0']); + $allowedClientC = $this->createClient($options, ['REMOTE_ADDR' => '203.0.113.0']); $this->ensureKernelShutdown(); - $barredClient = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['REMOTE_ADDR' => '192.168.1.1']); + $barredClient = $this->createClient($options, ['REMOTE_ADDR' => '192.168.1.1']); $this->assertAllowed($allowedClientA, '/secured-by-two-ips'); $this->assertAllowed($allowedClientB, '/secured-by-two-ips'); @@ -97,19 +97,19 @@ public function testSecurityConfigurationForMultipleIPAddresses($config) } /** - * @dataProvider getConfigs + * @dataProvider provideConfigs */ - public function testSecurityConfigurationForExpression($config) + public function testSecurityConfigurationForExpression(array $options) { - $allowedClient = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], ['HTTP_USER_AGENT' => 'Firefox 1.0']); + $allowedClient = $this->createClient($options, ['HTTP_USER_AGENT' => 'Firefox 1.0']); $this->assertAllowed($allowedClient, '/protected-via-expression'); $this->ensureKernelShutdown(); - $barredClient = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], []); + $barredClient = $this->createClient($options, []); $this->assertRestricted($barredClient, '/protected-via-expression'); $this->ensureKernelShutdown(); - $allowedClient = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => $config], []); + $allowedClient = $this->createClient($options, []); $allowedClient->request('GET', '/protected-via-expression'); $form = $allowedClient->followRedirect()->selectButton('login')->form(); @@ -131,12 +131,12 @@ public function testInvalidIpsInAccessControl() public function testPublicHomepage() { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']); + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'base_config.yml']); $client->request('GET', '/en/'); $this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse()); $this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public')); - $this->assertSame(0, self::$container->get('session')->getUsageIndex()); + $this->assertSame(0, self::getContainer()->get('request_tracker_subscriber')->getLastRequest()->getSession()->getUsageIndex()); } private function assertAllowed($client, $path) @@ -151,8 +151,9 @@ private function assertRestricted($client, $path) $this->assertEquals(302, $client->getResponse()->getStatusCode()); } - public function getConfigs() + public function provideConfigs() { - return [['config.yml'], ['routes_as_path.yml']]; + yield [['test_case' => 'StandardFormLogin', 'root_config' => 'base_config.yml']]; + yield [['test_case' => 'StandardFormLogin', 'root_config' => 'routes_as_path.yml']]; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php index 1f41e2646d1af..76c5e807232b7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php @@ -13,7 +13,8 @@ use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; class SecurityTest extends AbstractWebTestCase @@ -25,29 +26,15 @@ public function testServiceIsFunctional() $container = $kernel->getContainer(); // put a token into the storage so the final calls can function - $user = new User('foo', 'pass'); - $token = new UsernamePasswordToken($user, '', 'provider', ['ROLE_USER']); - $container->get('security.token_storage')->setToken($token); + $user = new InMemoryUser('foo', 'pass'); + $token = new UsernamePasswordToken($user, 'provider', ['ROLE_USER']); + $container->get('functional.test.security.token_storage')->setToken($token); $security = $container->get('functional_test.security.helper'); $this->assertTrue($security->isGranted('ROLE_USER')); $this->assertSame($token, $security->getToken()); } - public function userWillBeMarkedAsChangedIfRolesHasChangedProvider() - { - return [ - [ - new User('user1', 'test', ['ROLE_ADMIN']), - new User('user1', 'test', ['ROLE_USER']), - ], - [ - new UserWithoutEquatable('user1', 'test', ['ROLE_ADMIN']), - new UserWithoutEquatable('user1', 'test', ['ROLE_USER']), - ], - ]; - } - /** * @dataProvider userWillBeMarkedAsChangedIfRolesHasChangedProvider */ @@ -76,9 +63,23 @@ public function testUserWillBeMarkedAsChangedIfRolesHasChanged(UserInterface $us $client->request('GET', '/admin'); $this->assertEquals(302, $client->getResponse()->getStatusCode()); } + + public function userWillBeMarkedAsChangedIfRolesHasChangedProvider() + { + return [ + [ + new InMemoryUser('user1', 'test', ['ROLE_ADMIN']), + new InMemoryUser('user1', 'test', ['ROLE_USER']), + ], + [ + new UserWithoutEquatable('user1', 'test', ['ROLE_ADMIN']), + new UserWithoutEquatable('user1', 'test', ['ROLE_USER']), + ], + ]; + } } -final class UserWithoutEquatable implements UserInterface +final class UserWithoutEquatable implements UserInterface, PasswordAuthenticatedUserInterface { private $username; private $password; @@ -103,15 +104,15 @@ public function __construct(?string $username, ?string $password, array $roles = $this->roles = $roles; } - public function __toString() + public function __toString(): string { - return $this->getUsername(); + return $this->getUserIdentifier(); } /** * {@inheritdoc} */ - public function getRoles() + public function getRoles(): array { return $this->roles; } @@ -119,7 +120,7 @@ public function getRoles() /** * {@inheritdoc} */ - public function getPassword() + public function getPassword(): ?string { return $this->password; } @@ -127,15 +128,20 @@ public function getPassword() /** * {@inheritdoc} */ - public function getSalt() + public function getSalt(): string { - return null; + return ''; } /** * {@inheritdoc} */ - public function getUsername() + public function getUsername(): string + { + return $this->username; + } + + public function getUserIdentifier(): string { return $this->username; } @@ -143,7 +149,7 @@ public function getUsername() /** * {@inheritdoc} */ - public function isAccountNonExpired() + public function isAccountNonExpired(): bool { return $this->accountNonExpired; } @@ -151,7 +157,7 @@ public function isAccountNonExpired() /** * {@inheritdoc} */ - public function isAccountNonLocked() + public function isAccountNonLocked(): bool { return $this->accountNonLocked; } @@ -159,7 +165,7 @@ public function isAccountNonLocked() /** * {@inheritdoc} */ - public function isCredentialsNonExpired() + public function isCredentialsNonExpired(): bool { return $this->credentialsNonExpired; } @@ -167,7 +173,7 @@ public function isCredentialsNonExpired() /** * {@inheritdoc} */ - public function isEnabled() + public function isEnabled(): bool { return $this->enabled; } @@ -175,7 +181,7 @@ public function isEnabled() /** * {@inheritdoc} */ - public function eraseCredentials() + public function eraseCredentials(): void { } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php index 183b1ad8c4ef8..8b8848014d57a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php @@ -21,7 +21,7 @@ class SwitchUserTest extends AbstractWebTestCase */ public function testSwitchUser($originalUser, $targetUser, $expectedUser, $expectedStatus) { - $client = $this->createAuthenticatedClient($originalUser); + $client = $this->createAuthenticatedClient($originalUser, ['root_config' => 'switchuser.yml']); $client->request('GET', '/profile?_switch_user='.$targetUser); @@ -73,9 +73,9 @@ public function getTestParameters() ]; } - protected function createAuthenticatedClient($username) + protected function createAuthenticatedClient($username, array $options = []) { - $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'switchuser.yml']); + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'switchuser.yml'] + $options); $client->followRedirects(true); $form = $client->request('GET', '/login')->selectButton('login')->form(); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php deleted file mode 100644 index 78864da648876..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ /dev/null @@ -1,384 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests\Functional; - -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; -use Symfony\Component\Console\Application as ConsoleApplication; -use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; -use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; -use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; -use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; - -/** - * Tests UserPasswordEncoderCommand. - * - * @author Sarah Khalil - */ -class UserPasswordEncoderCommandTest extends AbstractWebTestCase -{ - /** @var CommandTester */ - private $passwordEncoderCommandTester; - private $colSize; - - public function testEncodePasswordEmptySalt() - { - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Symfony\Component\Security\Core\User\User', - '--empty-salt' => true, - ], ['decorated' => false]); - $expected = str_replace("\n", \PHP_EOL, file_get_contents(__DIR__.'/app/PasswordEncode/emptysalt.txt')); - - $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodeNoPasswordNoInteraction() - { - $statusCode = $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - ], ['interactive' => false]); - - $this->assertStringContainsString('[ERROR] The password must not be empty.', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertEquals(1, $statusCode); - } - - public function testEncodePasswordBcrypt() - { - $this->setupBcrypt(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Custom\Class\Bcrypt\User', - ], ['interactive' => false]); - - $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertStringContainsString('Password encoding succeeded', $output); - - $encoder = new NativePasswordEncoder(null, null, 17, \PASSWORD_BCRYPT); - preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches); - $hash = $matches[1]; - $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); - } - - public function testEncodePasswordArgon2i() - { - if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { - $this->markTestSkipped('Argon2i algorithm not available.'); - } - $this->setupArgon2i(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Custom\Class\Argon2i\User', - ], ['interactive' => false]); - - $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertStringContainsString('Password encoding succeeded', $output); - - $encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, \PASSWORD_ARGON2I); - preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); - $hash = $matches[1]; - $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); - } - - public function testEncodePasswordArgon2id() - { - if (!($sodium = (SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'))) && !\defined('PASSWORD_ARGON2ID')) { - $this->markTestSkipped('Argon2id algorithm not available.'); - } - $this->setupArgon2id(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Custom\Class\Argon2id\User', - ], ['interactive' => false]); - - $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertStringContainsString('Password encoding succeeded', $output); - - $encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, \PASSWORD_ARGON2ID); - preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); - $hash = $matches[1]; - $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); - } - - public function testEncodePasswordNative() - { - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Custom\Class\Native\User', - ], ['interactive' => false]); - - $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertStringContainsString('Password encoding succeeded', $output); - - $encoder = new NativePasswordEncoder(); - preg_match('# Encoded password\s{1,}([\w+\/$.,=]+={0,2})\s+#', $output, $matches); - $hash = $matches[1]; - $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); - } - - public function testEncodePasswordSodium() - { - if (!SodiumPasswordEncoder::isSupported()) { - $this->markTestSkipped('Libsodium is not available.'); - } - $this->setupSodium(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Custom\Class\Sodium\User', - ], ['interactive' => false]); - - $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertStringContainsString('Password encoding succeeded', $output); - - preg_match('# Encoded password\s+(\$?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); - $hash = $matches[1]; - $this->assertTrue((new SodiumPasswordEncoder())->isPasswordValid($hash, 'password', null)); - } - - public function testEncodePasswordPbkdf2() - { - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Custom\Class\Pbkdf2\User', - ], ['interactive' => false]); - - $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertStringContainsString('Password encoding succeeded', $output); - - $encoder = new Pbkdf2PasswordEncoder('sha512', true, 1000); - preg_match('# Encoded password\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); - $hash = $matches[1]; - preg_match('# Generated salt\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); - $salt = $matches[1]; - $this->assertTrue($encoder->isPasswordValid($hash, 'password', $salt)); - } - - public function testEncodePasswordOutput() - { - $this->passwordEncoderCommandTester->execute( - [ - 'command' => 'security:encode-password', - 'password' => 'p@ssw0rd', - ], ['interactive' => false] - ); - - $this->assertStringContainsString('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertStringContainsString(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertStringContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodePasswordEmptySaltOutput() - { - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'p@ssw0rd', - 'user-class' => 'Symfony\Component\Security\Core\User\User', - '--empty-salt' => true, - ]); - - $this->assertStringContainsString('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertStringContainsString(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodePasswordNativeOutput() - { - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'p@ssw0rd', - 'user-class' => 'Custom\Class\Native\User', - ], ['interactive' => false]); - - $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodePasswordArgon2iOutput() - { - if (!(SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { - $this->markTestSkipped('Argon2i algorithm not available.'); - } - - $this->setupArgon2i(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'p@ssw0rd', - 'user-class' => 'Custom\Class\Argon2i\User', - ], ['interactive' => false]); - - $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodePasswordArgon2idOutput() - { - if (!(SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2ID')) { - $this->markTestSkipped('Argon2id algorithm not available.'); - } - - $this->setupArgon2id(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'p@ssw0rd', - 'user-class' => 'Custom\Class\Argon2id\User', - ], ['interactive' => false]); - - $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodePasswordSodiumOutput() - { - if (!SodiumPasswordEncoder::isSupported()) { - $this->markTestSkipped('Libsodium is not available.'); - } - - $this->setupSodium(); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'p@ssw0rd', - 'user-class' => 'Custom\Class\Sodium\User', - ], ['interactive' => false]); - - $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testEncodePasswordNoConfigForGivenUserClass() - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('No encoder has been configured for account "Foo\Bar\User".'); - - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - 'user-class' => 'Foo\Bar\User', - ], ['interactive' => false]); - } - - public function testEncodePasswordAsksNonProvidedUserClass() - { - $this->passwordEncoderCommandTester->setInputs(['Custom\Class\Pbkdf2\User', "\n"]); - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - ], ['decorated' => false]); - - $this->assertStringContainsString(<<passwordEncoderCommandTester->getDisplay(true)); - } - - public function testNonInteractiveEncodePasswordUsesFirstUserClass() - { - $this->passwordEncoderCommandTester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - ], ['interactive' => false]); - - $this->assertStringContainsString('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $this->passwordEncoderCommandTester->getDisplay()); - } - - public function testThrowsExceptionOnNoConfiguredEncoders() - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('There are no configured encoders for the "security" extension.'); - $application = new ConsoleApplication(); - $application->add(new UserPasswordEncoderCommand($this->createMock(EncoderFactoryInterface::class), [])); - - $passwordEncoderCommand = $application->find('security:encode-password'); - - $tester = new CommandTester($passwordEncoderCommand); - $tester->execute([ - 'command' => 'security:encode-password', - 'password' => 'password', - ], ['interactive' => false]); - } - - protected function setUp(): void - { - $this->colSize = getenv('COLUMNS'); - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); - - $kernel = $this->createKernel(['test_case' => 'PasswordEncode']); - $kernel->boot(); - - $application = new Application($kernel); - - $passwordEncoderCommand = $application->get('security:encode-password'); - - $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); - } - - protected function tearDown(): void - { - $this->passwordEncoderCommandTester = null; - putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); - } - - private function setupArgon2i() - { - $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2i.yml']); - $kernel->boot(); - - $application = new Application($kernel); - - $passwordEncoderCommand = $application->get('security:encode-password'); - - $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); - } - - private function setupArgon2id() - { - $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']); - $kernel->boot(); - - $application = new Application($kernel); - - $passwordEncoderCommand = $application->get('security:encode-password'); - - $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); - } - - private function setupBcrypt() - { - $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'bcrypt.yml']); - $kernel->boot(); - - $application = new Application($kernel); - - $passwordEncoderCommand = $application->get('security:encode-password'); - - $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); - } - - private function setupSodium() - { - $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'sodium.yml']); - $kernel->boot(); - - $application = new Application($kernel); - - $passwordEncoderCommand = $application->get('security:encode-password'); - - $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml index 5c86da6252789..1cc13de77736f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml @@ -8,8 +8,9 @@ services: class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider security: + enable_authenticator_manager: true - encoders: + password_hashers: \Symfony\Component\Security\Core\User\UserInterface: plaintext providers: @@ -23,7 +24,6 @@ security: remember_me: true require_previous_session: false logout: ~ - anonymous: ~ stateless: false access_control: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/legacy_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/legacy_config.yml new file mode 100644 index 0000000000000..54bfaf89cb6c7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/legacy_config.yml @@ -0,0 +1,30 @@ +imports: + - { resource: ./../config/framework.yml } + +services: + _defaults: { public: true } + + security.user.provider.array: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider + +security: + password_hashers: + \Symfony\Component\Security\Core\User\UserInterface: plaintext + + providers: + array: + id: security.user.provider.array + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + logout: ~ + stateless: false + + access_control: + - { path: ^/admin$, roles: ROLE_ADMIN } + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml deleted file mode 100644 index 8ee417ab3a17d..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml +++ /dev/null @@ -1,24 +0,0 @@ -framework: - secret: test - router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } - validation: { enabled: true, enable_annotations: true } - csrf_protection: true - form: true - test: ~ - default_locale: en - session: - storage_id: session.storage.mock_file - profiler: { only_exceptions: false } - -services: - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator: ~ - -security: - firewalls: - secure: - pattern: ^/ - anonymous: false - stateless: true - guard: - authenticators: - - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml deleted file mode 100644 index 4d11154375219..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml +++ /dev/null @@ -1,5 +0,0 @@ -main: - path: / - defaults: - _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction - path: /app diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php index c4e7e4a15bce1..2839d5dfaff60 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\app; use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -36,10 +35,13 @@ public function __construct($varDir, $testCase, $rootConfig, $environment, $debu $this->testCase = $testCase; $fs = new Filesystem(); - if (!$fs->isAbsolutePath($rootConfig) && !is_file($rootConfig = __DIR__.'/'.$testCase.'/'.$rootConfig)) { - throw new \InvalidArgumentException(sprintf('The root config "%s" does not exist.', $rootConfig)); + foreach ((array) $rootConfig as $config) { + if (!$fs->isAbsolutePath($config) && !is_file($config = __DIR__.'/'.$testCase.'/'.$config)) { + throw new \InvalidArgumentException(sprintf('The root config "%s" does not exist.', $config)); + } + + $this->rootConfig[] = $config; } - $this->rootConfig = $rootConfig; parent::__construct($environment, $debug); } @@ -49,7 +51,7 @@ public function __construct($varDir, $testCase, $rootConfig, $environment, $debu */ public function getContainerClass(): string { - return parent::getContainerClass().substr(md5($this->rootConfig), -16); + return parent::getContainerClass().substr(md5(implode('', $this->rootConfig)), -16); } public function registerBundles(): iterable @@ -78,7 +80,9 @@ public function getLogDir(): string public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load($this->rootConfig); + foreach ($this->rootConfig as $config) { + $loader->load($config); + } } public function serialize() @@ -99,13 +103,4 @@ protected function getKernelParameters(): array return $parameters; } - - public function getContainer(): ContainerInterface - { - if (!$this->container) { - throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); - } - - return parent::getContainer(); - } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php similarity index 100% rename from src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/bundles.php diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml new file mode 100644 index 0000000000000..9bf5d2aa8201f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/config.yml @@ -0,0 +1,25 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } + test: ~ + default_locale: en + profiler: false + session: + storage_factory_id: session.storage.factory.mock_file + +services: + logger: { class: Psr\Log\NullLogger } + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController: + public: true + calls: + - ['setContainer', ['@Psr\Container\ContainerInterface']] + tags: [container.service_subscriber] + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\SecurityController: + public: true + calls: + - ['setContainer', ['@Psr\Container\ContainerInterface']] + tags: [container.service_subscriber] + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: ~ + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\LoginFormAuthenticator: + public: true + arguments: ['@router'] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml new file mode 100644 index 0000000000000..7822396eae16a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/firewall_user_provider.yml @@ -0,0 +1,11 @@ +imports: +- { resource: ./config.yml } +- { resource: ./security.yml } + +security: + enable_authenticator_manager: true + firewalls: + api: + pattern: / + provider: in_memory + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml new file mode 100644 index 0000000000000..b2433ecd35a1c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/implicit_user_provider.yml @@ -0,0 +1,10 @@ +imports: +- { resource: ./config.yml } +- { resource: ./security.yml } + +security: + enable_authenticator_manager: true + firewalls: + api: + pattern: / + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/multiple_firewall_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/multiple_firewall_user_provider.yml new file mode 100644 index 0000000000000..2630fd00b475f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/multiple_firewall_user_provider.yml @@ -0,0 +1,14 @@ +imports: +- { resource: ./config.yml } +- { resource: ./security.yml } + +security: + firewalls: + main: + pattern: ^/main + provider: in_memory + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\LoginFormAuthenticator + custom: + pattern: ^/custom + provider: in_memory2 + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\LoginFormAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/multiple_firewalls.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/multiple_firewalls.yml new file mode 100644 index 0000000000000..655a8d83d4d5f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/multiple_firewalls.yml @@ -0,0 +1,17 @@ +imports: +- { resource: ./config.yml } +- { resource: ./security.yml } + +security: + enable_authenticator_manager: true + firewalls: + firewall1: + pattern: /firewall1 + provider: in_memory + form_login: + check_path: /firewall1/login + firewall2: + pattern: /firewall2 + provider: in_memory2 + form_login: + check_path: /firewall2/login diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/no_user_provider.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/no_user_provider.yml new file mode 100644 index 0000000000000..3983d567c5572 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/no_user_provider.yml @@ -0,0 +1,14 @@ +imports: +- { resource: ./config.yml } + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator: + - true + +security: + enable_authenticator_manager: true + + firewalls: + api: + pattern: / + custom_authenticator: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ApiAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml new file mode 100644 index 0000000000000..4796a3f6bc00c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/routing.yml @@ -0,0 +1,28 @@ +profile: + path: /profile + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController + +security_main_login: + path: /main/login/check + defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\SecurityController::checkAction } + +security_custom_login: + path: /custom/login/check + defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\SecurityController::checkAction } + +security_main_profile: + path: /main/user_profile + defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\SecurityController::profileAction } + +security_custom_profile: + path: /custom/user_profile + defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\SecurityController::profileAction } + +firewall1_login: + path: /firewall1/login + +firewall2_profile: + path: /firewall2/profile + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AuthenticatorBundle\ProfileController diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/security.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/security.yml new file mode 100644 index 0000000000000..2a1d748ec2fb4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Authenticator/security.yml @@ -0,0 +1,15 @@ +security: + enable_authenticator_manager: true + + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + 'jane@example.org': { password: test, roles: [ROLE_USER] } + in_memory2: + memory: + users: + 'john@example.org': { password: test, roles: [ROLE_USER] } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/config.yml index 2045118e1b9f1..8be3ebc6436af 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/config.yml @@ -7,6 +7,7 @@ services: class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AutowiringBundle\AutowiredServices autowire: true security: + enable_authenticator_manager: true providers: dummy: memory: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/legacy_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/legacy_config.yml new file mode 100644 index 0000000000000..2045118e1b9f1 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/legacy_config.yml @@ -0,0 +1,15 @@ +imports: + - { resource: ../config/framework.yml } + +services: + _defaults: { public: true } + test.autowiring_types.autowired_services: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AutowiringBundle\AutowiredServices + autowire: true +security: + providers: + dummy: + memory: ~ + firewalls: + dummy: + security: false diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php deleted file mode 100644 index 9a26fb163a77d..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Bundle\FrameworkBundle\FrameworkBundle; -use Symfony\Bundle\SecurityBundle\SecurityBundle; - -return [ - new FrameworkBundle(), - new SecurityBundle(), -]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml deleted file mode 100644 index a0ed6f8e1e151..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml +++ /dev/null @@ -1,31 +0,0 @@ -imports: - - { resource: ./../config/framework.yml } - -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - providers: - in_memory: - memory: - users: - johannes: { password: test, roles: [ROLE_USER] } - - firewalls: - default: - form_login: - check_path: login - remember_me: true - remember_me: - always_remember_me: true - secret: key - anonymous: ~ - - access_control: - - { path: ^/foo, roles: ROLE_USER } - -services: - Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider: - public: true - decorates: security.user.provider.concrete.in_memory - arguments: ['@Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider.inner'] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml deleted file mode 100644 index 08975bdcb3832..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml +++ /dev/null @@ -1,7 +0,0 @@ -login: - path: /login - -foo: - path: /foo - defaults: - _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeFooController diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml new file mode 100644 index 0000000000000..945fd0fce3366 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/base_config.yml @@ -0,0 +1,50 @@ +imports: + - { resource: ./../config/default.yml } + +services: + csrf_form_login.form.type: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Form\UserLoginType + arguments: + - '@request_stack' + tags: + - { name: form.type } + + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Controller\LoginController: + public: true + tags: + - { name: container.service_subscriber } + +security: + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + # This firewall doesn't make sense in combination with the rest of the + # configuration file, but it's here for testing purposes (do not use + # this file in a real world scenario though) + login_form: + pattern: ^/login$ + security: false + + default: + form_login: + check_path: /login_check + default_target_path: /profile + target_path_parameter: "user_login[_target_path]" + failure_path_parameter: "user_login[_failure_path]" + username_parameter: "user_login[username]" + password_parameter: "user_login[password]" + logout: + path: /logout_path + target: / + csrf_token_generator: security.csrf.token_manager + + access_control: + - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml index 5a00ac329895d..ff265cac9e27d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml @@ -1,47 +1,10 @@ imports: - - { resource: ./../config/default.yml } - -services: - csrf_form_login.form.type: - class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\Form\UserLoginType - arguments: - - '@request_stack' - tags: - - { name: form.type } + - { resource: ./base_config.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - providers: - in_memory: - memory: - users: - johannes: { password: test, roles: [ROLE_USER] } - + enable_authenticator_manager: true firewalls: - # This firewall doesn't make sense in combination with the rest of the - # configuration file, but it's here for testing purposes (do not use - # this file in a real world scenario though) - login_form: - pattern: ^/login$ - security: false - default: form_login: - check_path: /login_check - default_target_path: /profile - target_path_parameter: "user_login[_target_path]" - failure_path_parameter: "user_login[_failure_path]" - username_parameter: "user_login[username]" - password_parameter: "user_login[password]" + enable_csrf: true csrf_parameter: "user_login[_token]" - csrf_token_generator: security.csrf.token_manager - anonymous: ~ - logout: - path: /logout_path - target: / - csrf_token_generator: security.csrf.token_manager - - access_control: - - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/routes_as_path.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/routes_as_path.yml index d481e6d2b7150..57abb3f2f6771 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/routes_as_path.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/routes_as_path.yml @@ -2,6 +2,7 @@ imports: - { resource: ./config.yml } security: + enable_authenticator_manager: true firewalls: default: form_login: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml index ae33d776e43fe..758364eaed248 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/config.yml @@ -1,26 +1,26 @@ framework: secret: test - router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } validation: { enabled: true, enable_annotations: true } csrf_protection: true - form: true + form: + enabled: true test: ~ default_locale: en session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file profiler: { only_exceptions: false } services: logger: { class: Psr\Log\NullLogger } security: + enable_authenticator_manager: true firewalls: secure: pattern: ^/secure/ http_basic: { realm: "Secure Gateway API" } entry_point: firewall_entry_point.entry_point.stub - default: - anonymous: ~ access_control: - { path: ^/secure/, roles: ROLE_SECURE } providers: @@ -28,5 +28,5 @@ security: memory: users: john: { password: doe, roles: [ROLE_SECURE] } - encoders: - Symfony\Component\Security\Core\User\User: plaintext + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php deleted file mode 100644 index d1e9eb7e0d36a..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php +++ /dev/null @@ -1,15 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -return [ - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\SecurityBundle\SecurityBundle(), -]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml deleted file mode 100644 index 7f87c307d28b5..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml +++ /dev/null @@ -1,33 +0,0 @@ -framework: - secret: test - router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } - test: ~ - default_locale: en - profiler: false - session: - storage_id: session.storage.mock_file - -services: - logger: { class: Psr\Log\NullLogger } - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator: ~ - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AuthenticationController: - tags: [controller.service_arguments] - -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - providers: - in_memory: - memory: - users: - Jane: { password: test, roles: [ROLE_USER] } - - firewalls: - secure: - pattern: ^/ - anonymous: lazy - stateless: false - guard: - authenticators: - - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml deleted file mode 100644 index 146aa811a143d..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml +++ /dev/null @@ -1,14 +0,0 @@ -main: - path: / - defaults: - _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction - path: /app -profile: - path: /profile - defaults: - _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AuthenticationController::profileAction - -manual_login: - path: /manual_login - defaults: - _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AuthenticationController::manualLoginAction diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml index 3522f27f13898..4a8cacb279b1f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml @@ -5,8 +5,9 @@ framework: serializer: ~ security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -17,7 +18,6 @@ security: firewalls: main: pattern: ^/ - anonymous: true json_login: check_path: /chk username_path: user.login diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml index e15e203c626cc..b8986de18f499 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml @@ -2,8 +2,9 @@ imports: - { resource: ./../config/framework.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -14,7 +15,6 @@ security: firewalls: main: pattern: ^/ - anonymous: true json_login: check_path: /chk username_path: user.login diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/legacy_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/legacy_config.yml new file mode 100644 index 0000000000000..d0d03c914c48f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/legacy_config.yml @@ -0,0 +1,26 @@ +imports: + - { resource: ./../config/framework.yml } + +framework: + serializer: ~ + +security: + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + dunglas: { password: foo, roles: [ROLE_USER] } + + firewalls: + main: + pattern: ^/ + json_login: + check_path: /chk + username_path: user.login + password_path: user.password + + access_control: + - { path: ^/foo, roles: ROLE_USER } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/legacy_custom_handlers.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/legacy_custom_handlers.yml new file mode 100644 index 0000000000000..f1f1a93ab0c0b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/legacy_custom_handlers.yml @@ -0,0 +1,31 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + dunglas: { password: foo, roles: [ROLE_USER] } + + firewalls: + main: + pattern: ^/ + json_login: + check_path: /chk + username_path: user.login + password_path: user.password + success_handler: json_login.success_handler + failure_handler: json_login.failure_handler + + access_control: + - { path: ^/foo, roles: ROLE_USER } + +services: + json_login.success_handler: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\Security\Http\JsonAuthenticationSuccessHandler + json_login.failure_handler: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\Security\Http\JsonAuthenticationFailureHandler diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/switchuser_stateless.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/switchuser_stateless.yml index 84a0493e050b2..e2bebd525f488 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/switchuser_stateless.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/switchuser_stateless.yml @@ -2,6 +2,7 @@ imports: - { resource: ./config.yml } security: + enable_authenticator_manager: true providers: in_memory: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml index 80d5ec570e29d..67234430451db 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml @@ -12,6 +12,7 @@ services: protocol_version: 3 referrals: false security: + enable_authenticator_manager: true providers: ldap: ldap: @@ -27,7 +28,6 @@ security: main: pattern: ^/login stateless: true - anonymous: true json_login_ldap: check_path: /login require_previous_session: false diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/bundles.php new file mode 100644 index 0000000000000..bcfd17425cfd1 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\SecurityBundle\SecurityBundle(), + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/config.yml new file mode 100644 index 0000000000000..969c669eabcfc --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/config.yml @@ -0,0 +1,28 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + enable_authenticator_manager: true + + providers: + in_memory: + memory: + users: + weaverryan: { password: foo, roles: [ROLE_USER] } + + firewalls: + main: + pattern: ^/ + login_link: + check_route: login_link_check + signature_properties: ['password'] + max_uses: 2 + success_handler: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\LoginLink\TestCustomLoginLinkSuccessHandler + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\LoginLink\TestCustomLoginLinkSuccessHandler: null + # needed so LoginLinkHandlerInterface has *some* reference, and so isn't + # entirely removed from the container (so we can fetch it in the test container) + login_link_handler: + alias: 'Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface' + public: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/routing.yml new file mode 100644 index 0000000000000..890d2bef3d4c7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LoginLink/routing.yml @@ -0,0 +1,2 @@ +login_link_check: + path: /login-link-check diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/bundles.php similarity index 100% rename from src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/bundles.php diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/config_access.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/config_access.yml new file mode 100644 index 0000000000000..fbcb7e6defc79 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/config_access.yml @@ -0,0 +1,27 @@ +imports: +- { resource: ./../config/framework.yml } + +security: + enable_authenticator_manager: true + + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + logout: ~ + stateless: true + + access_control: + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/config_cookie_clearing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/config_cookie_clearing.yml new file mode 100644 index 0000000000000..974f0ab79df64 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/config_cookie_clearing.yml @@ -0,0 +1,28 @@ +imports: +- { resource: ./../config/framework.yml } + +security: + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + logout: + delete_cookies: + flavor: { path: null, domain: somedomain } + stateless: true + + access_control: + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/routing.yml similarity index 100% rename from src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/routing.yml rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Logout/routing.yml diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/config.yml deleted file mode 100644 index 2e20735b80236..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/config.yml +++ /dev/null @@ -1,26 +0,0 @@ -imports: -- { resource: ./../config/framework.yml } - -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - providers: - in_memory: - memory: - users: - johannes: { password: test, roles: [ROLE_USER] } - - firewalls: - default: - form_login: - check_path: login - remember_me: true - require_previous_session: false - logout: ~ - anonymous: ~ - stateless: true - - access_control: - - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml index 9e5563fea5197..1dd8b8e507d36 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/config.yml @@ -2,8 +2,10 @@ imports: - { resource: ./../config/framework.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -22,5 +24,4 @@ security: secret: secret logout: invalidate_session: false - anonymous: ~ stateless: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/config.yml index 501a673b4fdea..ec3839a76adcb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/config.yml @@ -2,6 +2,7 @@ imports: - { resource: ./../config/framework.yml } security: + enable_authenticator_manager: true firewalls: default: http_basic: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2i.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2i.yml deleted file mode 100644 index 2ca4f3461a6e9..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2i.yml +++ /dev/null @@ -1,7 +0,0 @@ -imports: - - { resource: config.yml } - -security: - encoders: - Custom\Class\Argon2i\User: - algorithm: argon2i diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml deleted file mode 100644 index 481262acb7e6c..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml +++ /dev/null @@ -1,7 +0,0 @@ -imports: - - { resource: config.yml } - -security: - encoders: - Custom\Class\Argon2id\User: - algorithm: argon2id diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.yml deleted file mode 100644 index 1928c0400b722..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bcrypt.yml +++ /dev/null @@ -1,7 +0,0 @@ -imports: - - { resource: config.yml } - -security: - encoders: - Custom\Class\Bcrypt\User: - algorithm: bcrypt diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php deleted file mode 100644 index edf6dae14c064..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -return [ - new Symfony\Bundle\SecurityBundle\SecurityBundle(), - new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), -]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml deleted file mode 100644 index 9ae5433246561..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/config.yml +++ /dev/null @@ -1,27 +0,0 @@ -imports: - - { resource: ./../config/framework.yml } - -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - Custom\Class\Native\User: - algorithm: native - cost: 10 - Custom\Class\Pbkdf2\User: - algorithm: pbkdf2 - hash_algorithm: sha512 - encode_as_base64: true - iterations: 1000 - Custom\Class\Test\User: test - - providers: - in_memory: - memory: - users: - user: { password: userpass, roles: [ 'ROLE_USER' ] } - admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } - - firewalls: - test: - pattern: ^/ - security: false diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt deleted file mode 100644 index 9c8d3deb1b462..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/emptysalt.txt +++ /dev/null @@ -1,13 +0,0 @@ - -Symfony Password Encoder Utility -================================ - - ------------------ ------------------------------------------------------------------ - Key Value - ------------------ ------------------------------------------------------------------ - Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder - Encoded password password - ------------------ ------------------------------------------------------------------ - - [OK] Password encoding succeeded - diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml deleted file mode 100644 index 1ccc2a10d5f6e..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/sodium.yml +++ /dev/null @@ -1,7 +0,0 @@ -imports: - - { resource: config.yml } - -security: - encoders: - Custom\Class\Sodium\User: - algorithm: sodium diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/bundles.php new file mode 100644 index 0000000000000..341dac04c2649 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/bundles.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\RememberMeBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), + new RememberMeBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/clear_on_change_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/clear_on_change_config.yml new file mode 100644 index 0000000000000..b01603b3f6aa7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/clear_on_change_config.yml @@ -0,0 +1,9 @@ +imports: + - { resource: ./config.yml } + - { resource: ./config_session.yml } + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security\UserChangingUserProvider: + public: true + decorates: security.user.provider.concrete.in_memory + arguments: ['@.inner'] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config.yml new file mode 100644 index 0000000000000..fe52f22500606 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config.yml @@ -0,0 +1,23 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + logout: ~ + form_login: + check_path: login + remember_me: true + + access_control: + - { path: ^/profile, roles: ROLE_USER } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config_persistent.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config_persistent.yml new file mode 100644 index 0000000000000..40ded00c5539c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config_persistent.yml @@ -0,0 +1,13 @@ +services: + app.static_token_provider: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security\StaticTokenProvider + arguments: ['@kernel'] + +security: + enable_authenticator_manager: true + firewalls: + default: + remember_me: + always_remember_me: true + secret: key + token_provider: app.static_token_provider diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config_session.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config_session.yml new file mode 100644 index 0000000000000..a11750e6f60be --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/config_session.yml @@ -0,0 +1,7 @@ +security: + enable_authenticator_manager: true + firewalls: + default: + remember_me: + always_remember_me: true + secret: key diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/routing.yml new file mode 100644 index 0000000000000..a4f97930a2535 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/routing.yml @@ -0,0 +1,9 @@ +login: + path: /login + +logout: + path: /logout + +profile: + path: /profile + controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Controller\ProfileController diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/stateless_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/stateless_config.yml new file mode 100644 index 0000000000000..cf9102da35a08 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMe/stateless_config.yml @@ -0,0 +1,14 @@ +imports: + - { resource: ./config.yml } + - { resource: ./config_session.yml } + +framework: + session: + cookie_secure: auto + cookie_samesite: lax + +security: + enable_authenticator_manager: true + firewalls: + default: + stateless: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml index 8ffb7d8842ca7..4df09bad41582 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml @@ -2,8 +2,9 @@ imports: - { resource: ./../config/framework.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -22,4 +23,3 @@ security: secret: key secure: auto logout: ~ - anonymous: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php deleted file mode 100644 index a52ae15f6d9bd..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Bundle\FrameworkBundle\FrameworkBundle; -use Symfony\Bundle\SecurityBundle\SecurityBundle; -use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; - -return [ - new FrameworkBundle(), - new SecurityBundle(), - new TestBundle(), -]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml deleted file mode 100644 index 78857765160d9..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/config.yml +++ /dev/null @@ -1,30 +0,0 @@ -imports: - - { resource: ./../config/framework.yml } - -framework: - session: - cookie_secure: auto - cookie_samesite: lax - -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - providers: - in_memory: - memory: - users: - johannes: { password: test, roles: [ROLE_USER] } - - firewalls: - default: - form_login: - check_path: login - remember_me: true - require_previous_session: false - remember_me: - always_remember_me: true - secret: key - logout: ~ - anonymous: ~ - stateless: true diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml deleted file mode 100644 index 1dddfca2f8154..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/routing.yml +++ /dev/null @@ -1,5 +0,0 @@ -login: - path: /login - -logout: - path: /logout diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml index e49a697e52ebe..f2ac6ebde364f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml @@ -7,7 +7,12 @@ services: alias: security.helper public: true + functional.test.security.token_storage: + alias: security.token_storage + public: true + security: + enable_authenticator_manager: true providers: in_memory: memory: @@ -15,4 +20,3 @@ security: firewalls: default: - anonymous: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/legacy_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/legacy_config.yml new file mode 100644 index 0000000000000..01aa24889faf0 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/legacy_config.yml @@ -0,0 +1,22 @@ +imports: + - { resource: ./../config/framework.yml } + +services: + # alias the service so we can access it in the tests + functional_test.security.helper: + alias: security.helper + public: true + + functional.test.security.token_storage: + alias: security.token_storage + public: true + +security: + providers: + in_memory: + memory: + users: [] + + firewalls: + default: + anonymous: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml new file mode 100644 index 0000000000000..a243ec5f0a448 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/base_config.yml @@ -0,0 +1,57 @@ +imports: + - { resource: ./../config/default.yml } + +parameters: + env(APP_IP): '127.0.0.1' + env(APP_IPS): '127.0.0.1, ::1' + +security: + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + # This firewall doesn't make sense in combination with the rest of the + # configuration file, but it's here for testing purposes (do not use + # this file in a real world scenario though) + login_form: + pattern: ^/login$ + security: false + + default: + form_login: + check_path: /login_check + default_target_path: /profile + logout: ~ + lazy: true + + # This firewall is here just to check its the logout functionality + second_area: + http_basic: ~ + logout: + target: /second/target + path: /second/logout + + access_control: + - { path: ^/en/$, roles: PUBLIC_ACCESS } + - { path: ^/unprotected_resource$, roles: PUBLIC_ACCESS } + - { path: ^/secure-but-not-covered-by-access-control$, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: PUBLIC_ACCESS } + # these real IP addresses are reserved for docs/examples (https://tools.ietf.org/search/rfc5737) + - { path: ^/secured-by-one-real-ip$, ips: 198.51.100.0, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-real-ip-with-mask$, ips: '203.0.113.0/24', roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-real-ipv6$, ips: 0:0:0:0:0:ffff:c633:6400, roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder$, ips: '%env(APP_IP)%', roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder-multiple-ips$, ips: '%env(APP_IPS)%', roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder-and-one-real-ip$, ips: ['%env(APP_IP)%', 198.51.100.0], roles: PUBLIC_ACCESS } + - { path: ^/secured-by-one-env-placeholder-multiple-ips-and-one-real-ip$, ips: ['%env(APP_IPS)%', 198.51.100.0], roles: PUBLIC_ACCESS } + - { path: ^/highly_protected_resource$, roles: IS_ADMIN } + - { path: ^/protected-via-expression$, allow_if: "(!is_authenticated() and request.headers.get('user-agent') matches '/Firefox/i') or is_granted('ROLE_USER')" } + - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php index cef48bfcc4b46..6237258c8606e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php @@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RequestTrackerBundle\RequestTrackerBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -20,5 +21,6 @@ new SecurityBundle(), new TwigBundle(), new FormLoginBundle(), + new RequestTrackerBundle(), new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml deleted file mode 100644 index ad8beee94c2e0..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ /dev/null @@ -1,54 +0,0 @@ -imports: - - { resource: ./../config/default.yml } - -parameters: - env(APP_IP): '127.0.0.1' - -security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext - - providers: - in_memory: - memory: - users: - johannes: { password: test, roles: [ROLE_USER] } - - firewalls: - # This firewall doesn't make sense in combination with the rest of the - # configuration file, but it's here for testing purposes (do not use - # this file in a real world scenario though) - login_form: - pattern: ^/login$ - security: false - - default: - form_login: - check_path: /login_check - default_target_path: /profile - logout: ~ - anonymous: lazy - - # This firewall is here just to check its the logout functionality - second_area: - http_basic: ~ - anonymous: ~ - logout: - target: /second/target - path: /second/logout - - access_control: - - { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: IS_AUTHENTICATED_ANONYMOUSLY } - # these real IP addresses are reserved for docs/examples (https://tools.ietf.org/search/rfc5737) - - { path: ^/secured-by-one-real-ip$, ips: 198.51.100.0, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-real-ip-with-mask$, ips: '203.0.113.0/24', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-real-ipv6$, ips: 0:0:0:0:0:ffff:c633:6400, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-env-placeholder$, ips: '%env(APP_IP)%', roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/secured-by-one-env-placeholder-and-one-real-ip$, ips: ['%env(APP_IP)%', 198.51.100.0], roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/highly_protected_resource$, roles: IS_ADMIN } - - { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and request.headers.get('user-agent') matches '/Firefox/i') or is_granted('ROLE_USER')" } - - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/invalid_ip_access_control.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/invalid_ip_access_control.yml index cc6503affb265..0f190d9b6d1e4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/invalid_ip_access_control.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/invalid_ip_access_control.yml @@ -2,8 +2,9 @@ imports: - { resource: ./../config/default.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -15,7 +16,6 @@ security: default: form_login: ~ logout: ~ - anonymous: ~ access_control: # the '256.357.458.559' IP is wrong on purpose, to check invalid IP errors diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_form_failure_handler.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_form_failure_handler.yml index e01ed369b1f56..95603e583534d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_form_failure_handler.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_form_failure_handler.yml @@ -2,8 +2,9 @@ imports: - { resource: ./../config/default.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -17,4 +18,3 @@ security: login_path: localized_login_path check_path: localized_check_path failure_handler: localized_form_failure_handler - anonymous: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes.yml index 5251fd1d93de1..42f18b392d020 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes.yml @@ -2,8 +2,9 @@ imports: - { resource: ./../config/default.yml } security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext providers: in_memory: @@ -20,7 +21,6 @@ security: logout: path: localized_logout_path target: localized_logout_target_path - anonymous: ~ access_control: - { path: '^/(?:[a-z]{2})/secure/.*', roles: ROLE_USER } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes_with_forward.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes_with_forward.yml index 12d90d8835858..9cbfe5dae7159 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes_with_forward.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/localized_routes_with_forward.yml @@ -2,6 +2,7 @@ imports: - { resource: ./localized_routes.yml } security: + enable_authenticator_manager: true firewalls: default: form_login: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/login_throttling.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/login_throttling.yml new file mode 100644 index 0000000000000..fa94d30dc00cc --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/login_throttling.yml @@ -0,0 +1,14 @@ +imports: + - { resource: ./base_config.yml } + +framework: + lock: ~ + rate_limiter: ~ + +security: + enable_authenticator_manager: true + firewalls: + default: + login_throttling: + max_attempts: 1 + interval: '8 minutes' diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/routes_as_path.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/routes_as_path.yml index d481e6d2b7150..435951968d773 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/routes_as_path.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/routes_as_path.yml @@ -1,7 +1,8 @@ imports: - - { resource: ./config.yml } + - { resource: ./base_config.yml } security: + enable_authenticator_manager: true firewalls: default: form_login: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/switchuser.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/switchuser.yml index 2f144aae9fd80..4806ed5e0cb5d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/switchuser.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/switchuser.yml @@ -1,7 +1,8 @@ imports: - - { resource: ./config.yml } + - { resource: ./base_config.yml } security: + enable_authenticator_manager: true providers: in_memory: memory: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml index a6ee6533e8100..55186cd76bd3f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/framework.yml @@ -1,14 +1,15 @@ framework: secret: test - router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml", utf8: true } validation: { enabled: true, enable_annotations: true } assets: ~ csrf_protection: true - form: true + form: + enabled: true test: ~ default_locale: en session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file profiler: { only_exceptions: false } services: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml index f578e4b510378..493989866a278 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml @@ -2,4 +2,3 @@ twig: debug: '%kernel.debug%' strict_variables: '%kernel.debug%' - exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php new file mode 100644 index 0000000000000..0b466d0af7990 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/LoginLink/FirewallAwareLoginLinkHandlerTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\LoginLink; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Bundle\SecurityBundle\LoginLink\FirewallAwareLoginLinkHandler; +use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\LoginLink\LoginLinkDetails; +use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + +class FirewallAwareLoginLinkHandlerTest extends TestCase +{ + public function testSuccessfulDecoration() + { + $user = $this->createMock(UserInterface::class); + $linkDetails = new LoginLinkDetails('http://example.com', new \DateTimeImmutable()); + $request = Request::create('http://example.com/verify'); + + $firewallMap = $this->createFirewallMap('main_firewall'); + $loginLinkHandler = $this->createMock(LoginLinkHandlerInterface::class); + $loginLinkHandler->expects($this->once()) + ->method('createLoginLink') + ->with($user, $request) + ->willReturn($linkDetails); + $loginLinkHandler->expects($this->once()) + ->method('consumeLoginLink') + ->with($request) + ->willReturn($user); + $locator = $this->createLocator([ + 'main_firewall' => $loginLinkHandler, + ]); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $linker = new FirewallAwareLoginLinkHandler($firewallMap, $locator, $requestStack); + $actualLinkDetails = $linker->createLoginLink($user, $request); + $this->assertSame($linkDetails, $actualLinkDetails); + + $actualUser = $linker->consumeLoginLink($request); + $this->assertSame($user, $actualUser); + } + + private function createFirewallMap(string $firewallName) + { + $map = $this->createMock(FirewallMap::class); + $map->expects($this->any()) + ->method('getFirewallConfig') + ->willReturn($config = new FirewallConfig($firewallName, 'user_checker')); + + return $map; + } + + private function createLocator(array $linkers) + { + $locator = $this->createMock(ContainerInterface::class); + $locator->expects($this->any()) + ->method('has') + ->willReturnCallback(function ($firewallName) use ($linkers) { + return isset($linkers[$firewallName]); + }); + $locator->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($firewallName) use ($linkers) { + return $linkers[$firewallName]; + }); + + return $locator; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php index 99e897aa8ff20..59cb0fcc94e91 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php @@ -18,7 +18,7 @@ class FirewallConfigTest extends TestCase { public function testGetters() { - $listeners = ['logout', 'remember_me', 'anonymous']; + $authenticators = ['form_login', 'remember_me']; $options = [ 'request_matcher' => 'foo_request_matcher', 'security' => false, @@ -43,7 +43,7 @@ public function testGetters() $options['entry_point'], $options['access_denied_handler'], $options['access_denied_url'], - $listeners, + $authenticators, $options['switch_user'] ); @@ -57,8 +57,7 @@ public function testGetters() $this->assertSame($options['access_denied_handler'], $config->getAccessDeniedHandler()); $this->assertSame($options['access_denied_url'], $config->getAccessDeniedUrl()); $this->assertSame($options['user_checker'], $config->getUserChecker()); - $this->assertTrue($config->allowsAnonymous()); - $this->assertSame($listeners, $config->getListeners()); + $this->assertSame($authenticators, $config->getAuthenticators()); $this->assertSame($options['switch_user'], $config->getSwitchUser()); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php index 15f8764416099..c76783b7e08ad 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallContextTest.php @@ -34,15 +34,6 @@ public function testGetters() $this->assertEquals($config, $context->getConfig()); } - /** - * @group legacy - * @expectedDeprecation Passing an instance of Symfony\Bundle\SecurityBundle\Security\FirewallConfig as the 3rd argument to "Symfony\Bundle\SecurityBundle\Security\FirewallContext::__construct()" is deprecated since Symfony 4.2. Pass a Symfony\Component\Security\Http\Firewall\LogoutListener instance instead. - */ - public function testFirewallConfigAs3rdConstructorArgument() - { - new FirewallContext([], $this->getExceptionListenerMock(), new FirewallConfig('main', 'user_checker', 'request_matcher')); - } - private function getExceptionListenerMock() { return $this diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/UserAuthenticatorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/UserAuthenticatorTest.php new file mode 100644 index 0000000000000..ecb9cd8e7e33d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/UserAuthenticatorTest.php @@ -0,0 +1,34 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot determine the correct Symfony\Bundle\SecurityBundle\Security\UserAuthenticator to use: there is no active Request and so, the firewall cannot be determined. Try using a specific Symfony\Bundle\SecurityBundle\Security\UserAuthenticator service.'); + + $userAuthenticator->authenticateUser($user, $authenticator, $request); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php deleted file mode 100644 index c00b5d9e25267..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\SecurityBundle\SecurityUserValueResolver; -use Symfony\Bundle\SecurityBundle\Tests\Fixtures\TokenInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; -use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * @group legacy - */ -class SecurityUserValueResolverTest extends TestCase -{ - public function testResolveNoToken() - { - $tokenStorage = new TokenStorage(); - $resolver = new SecurityUserValueResolver($tokenStorage); - $metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null); - - $this->assertFalse($resolver->supports(Request::create('/'), $metadata)); - } - - public function testResolveNoUser() - { - $mock = $this->createMock(UserInterface::class); - $token = $this->createMock(TokenInterface::class); - $tokenStorage = new TokenStorage(); - $tokenStorage->setToken($token); - - $resolver = new SecurityUserValueResolver($tokenStorage); - $metadata = new ArgumentMetadata('foo', \get_class($mock), false, false, null); - - $this->assertFalse($resolver->supports(Request::create('/'), $metadata)); - } - - public function testResolveWrongType() - { - $tokenStorage = new TokenStorage(); - $resolver = new SecurityUserValueResolver($tokenStorage); - $metadata = new ArgumentMetadata('foo', null, false, false, null); - - $this->assertFalse($resolver->supports(Request::create('/'), $metadata)); - } - - public function testResolve() - { - $user = $this->createMock(UserInterface::class); - $token = $this->createMock(TokenInterface::class); - $token->expects($this->any())->method('getUser')->willReturn($user); - $tokenStorage = new TokenStorage(); - $tokenStorage->setToken($token); - - $resolver = new SecurityUserValueResolver($tokenStorage); - $metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null); - - $this->assertTrue($resolver->supports(Request::create('/'), $metadata)); - $this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata))); - } - - public function testIntegration() - { - $user = $this->createMock(UserInterface::class); - $token = $this->createMock(TokenInterface::class); - $token->expects($this->any())->method('getUser')->willReturn($user); - $tokenStorage = new TokenStorage(); - $tokenStorage->setToken($token); - - $argumentResolver = new ArgumentResolver(null, [new SecurityUserValueResolver($tokenStorage)]); - $this->assertSame([$user], $argumentResolver->getArguments(Request::create('/'), function (UserInterface $user) {})); - } - - public function testIntegrationNoUser() - { - $token = $this->createMock(TokenInterface::class); - $tokenStorage = new TokenStorage(); - $tokenStorage->setToken($token); - - $argumentResolver = new ArgumentResolver(null, [new SecurityUserValueResolver($tokenStorage), new DefaultValueResolver()]); - $this->assertSame([null], $argumentResolver->getArguments(Request::create('/'), function (UserInterface $user = null) {})); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 1106acfa008c6..9f928c2c4fb57 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -16,42 +16,46 @@ } ], "require": { - "php": ">=7.1.3", + "php": ">=8.0.2", + "composer-runtime-api": ">=2.1", "ext-xml": "*", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/polyfill-php80": "^1.16", - "symfony/security-core": "^4.4", - "symfony/security-csrf": "^4.2|^5.0", - "symfony/security-guard": "^4.2|^5.0", - "symfony/security-http": "^4.4.5" + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/security-core": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^5.4|^6.0" }, "require-dev": { "doctrine/annotations": "^1.10.4", - "symfony/asset": "^3.4|^4.0|^5.0", - "symfony/browser-kit": "^4.2|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/css-selector": "^3.4|^4.0|^5.0", - "symfony/dom-crawler": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/form": "^3.4|^4.0|^5.0", - "symfony/framework-bundle": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/serializer": "^4.4|^5.0", - "symfony/translation": "^3.4|^4.0|^5.0", - "symfony/twig-bridge": "^3.4|^4.0|^5.0", - "symfony/twig-bundle": "^4.4|^5.0", - "symfony/validator": "^3.4|^4.0|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0", - "twig/twig": "^1.43|^2.13|^3.0.4" + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "symfony/browser-kit": "<4.2", - "symfony/console": "<3.4", - "symfony/framework-bundle": "<4.4", - "symfony/ldap": "<4.4", - "symfony/twig-bundle": "<4.4" + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<5.4", + "symfony/ldap": "<5.4", + "symfony/twig-bundle": "<5.4" }, "autoload": { "psr-4": { "Symfony\\Bundle\\SecurityBundle\\": "" }, diff --git a/src/Symfony/Bundle/SecurityBundle/phpunit.xml.dist b/src/Symfony/Bundle/SecurityBundle/phpunit.xml.dist index 0824bf04c1514..b8b8a9adbedc1 100644 --- a/src/Symfony/Bundle/SecurityBundle/phpunit.xml.dist +++ b/src/Symfony/Bundle/SecurityBundle/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Resources - ./Tests - ./vendor - - - + + + ./Resources + ./Tests + ./vendor + + diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index 780c46466dd36..8bdf7748ff7d0 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -1,6 +1,31 @@ CHANGELOG ========= +6.0 +--- + + * The `twig` service is now private + +5.3 +--- + + * Add support for the new `serialize` filter (from Twig Bridge) + +5.2.0 +----- + + * deprecated the public `twig` service to private + +5.0.0 +----- + + * updated default value for the `strict_variables` option to `%kernel.debug%` parameter + * removed support to load templates from the legacy directories `src/Resources/views/` and `src/Resources//views/` + * removed `TwigEngine` class, use `Twig\Environment` instead + * removed `FilesystemLoader` and `NativeFilesystemLoader`, use Twig notation for templates instead + * removed `twig.exception_controller` configuration option, use `framework.error_controller` option instead + * removed `ExceptionController`, `PreviewErrorController` and all built-in error templates in favor of the new error renderer mechanism + 4.4.0 ----- @@ -18,7 +43,7 @@ CHANGELOG 4.1.0 ----- - * added priority to Twig extensions + * added priority to Twig extensions * deprecated relying on the default value (`false`) of the `twig.strict_variables` configuration option. The `%kernel.debug%` parameter will be the new default in 5.0 4.0.0 diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php deleted file mode 100644 index 9fe4b42438453..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\CacheWarmer; - -@trigger_error('The '.TemplateCacheCacheWarmer::class.' class is deprecated since version 4.4 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - -use Psr\Container\ContainerInterface; -use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinderInterface; -use Symfony\Bundle\TwigBundle\DependencyInjection\CompatibilityServiceSubscriberInterface as ServiceSubscriberInterface; -use Symfony\Component\Finder\Finder; -use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; -use Twig\Environment; -use Twig\Error\Error; - -/** - * Generates the Twig cache for all templates. - * - * This warmer must be registered after TemplatePathsCacheWarmer, - * as the Twig loader will need the cache generated by it. - * - * @author Fabien Potencier - * - * @deprecated since version 4.4, to be removed in 5.0; use Twig instead. - */ -class TemplateCacheCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInterface -{ - protected $container; - protected $finder; - private $paths; - - /** - * @param array $paths Additional twig paths to warm - */ - public function __construct(ContainerInterface $container, TemplateFinderInterface $finder = null, array $paths = []) - { - // We don't inject the Twig environment directly as it depends on the - // template locator (via the loader) which might be a cached one. - // The cached template locator is available once the TemplatePathsCacheWarmer - // has been warmed up. - // But it can also be null if templating has been disabled. - $this->container = $container; - $this->finder = $finder; - $this->paths = $paths; - } - - /** - * Warms up the cache. - * - * @param string $cacheDir The cache directory - */ - public function warmUp($cacheDir) - { - if (null === $this->finder) { - return; - } - - $twig = $this->container->get('twig'); - - $templates = $this->finder->findAllTemplates(); - - foreach ($this->paths as $path => $namespace) { - $templates = array_merge($templates, $this->findTemplatesInFolder($namespace, $path)); - } - - foreach ($templates as $template) { - try { - $twig->load($template); - } catch (Error $e) { - // problem during compilation, give up - } - } - } - - /** - * Checks whether this warmer is optional or not. - * - * @return bool always true - */ - public function isOptional() - { - return true; - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedServices() - { - return [ - 'twig' => Environment::class, - ]; - } - - /** - * Find templates in the given directory. - */ - private function findTemplatesInFolder(?string $namespace, string $dir): array - { - if (!is_dir($dir)) { - return []; - } - - $templates = []; - $finder = new Finder(); - - foreach ($finder->files()->followLinks()->in($dir) as $file) { - $name = $file->getRelativePathname(); - $templates[] = $namespace ? sprintf('@%s/%s', $namespace, $name) : $name; - } - - return $templates; - } -} diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php index e81ac2ab859d9..1d88c5d73eb31 100644 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php +++ b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php @@ -12,8 +12,8 @@ namespace Symfony\Bundle\TwigBundle\CacheWarmer; use Psr\Container\ContainerInterface; -use Symfony\Bundle\TwigBundle\DependencyInjection\CompatibilityServiceSubscriberInterface as ServiceSubscriberInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; use Twig\Environment; use Twig\Error\Error; @@ -26,7 +26,7 @@ class TemplateCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInte { private $container; private $twig; - private $iterator; + private iterable $iterator; public function __construct(ContainerInterface $container, iterable $iterator) { @@ -37,27 +37,41 @@ public function __construct(ContainerInterface $container, iterable $iterator) /** * {@inheritdoc} + * + * @return string[] A list of template files to preload on PHP 7.4+ */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir): array { - if (null === $this->twig) { - $this->twig = $this->container->get('twig'); - } + $this->twig ??= $this->container->get('twig'); + + $files = []; foreach ($this->iterator as $template) { try { - $this->twig->load($template); + $template = $this->twig->load($template); + + if (\is_callable([$template, 'unwrap'])) { + $files[] = (new \ReflectionClass($template->unwrap()))->getFileName(); + } } catch (Error $e) { - // problem during compilation, give up - // might be a syntax error or a non-Twig template + /* + * Problem during compilation, give up for this template (e.g. syntax errors). + * Failing silently here allows to ignore templates that rely on functions that aren't available in + * the current environment. For example, the WebProfilerBundle shouldn't be available in the prod + * environment, but some templates that are never used in prod might rely on functions the bundle provides. + * As we can't detect which templates are "really" important, we try to load all of them and ignore + * errors. Error checks may be performed by calling the lint:twig command. + */ } } + + return $files; } /** * {@inheritdoc} */ - public function isOptional() + public function isOptional(): bool { return true; } @@ -65,7 +79,7 @@ public function isOptional() /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'twig' => Environment::class, diff --git a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php index cb72bc4bf78a8..c9509c6582f82 100644 --- a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php +++ b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\TwigBundle\Command; use Symfony\Bridge\Twig\Command\LintCommand as BaseLintCommand; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Finder\Finder; /** @@ -20,6 +21,7 @@ * @author Marc Weistroff * @author Jérôme Tamarelle */ +#[AsCommand(name: 'lint:twig', description: 'Lint a Twig template and outputs encountered errors')] final class LintCommand extends BaseLintCommand { /** @@ -42,7 +44,7 @@ protected function configure() ; } - protected function findFiles($filename): iterable + protected function findFiles(string $filename): iterable { if (str_starts_with($filename, '@')) { $dir = $this->getApplication()->getKernel()->locateResource($filename); diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php deleted file mode 100644 index dda4ae6e82f32..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ /dev/null @@ -1,152 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\Controller; - -use Symfony\Component\Debug\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; -use Twig\Environment; -use Twig\Error\LoaderError; -use Twig\Loader\ExistsLoaderInterface; -use Twig\Loader\SourceContextLoaderInterface; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), \E_USER_DEPRECATED); - -/** - * ExceptionController renders error or exception pages for a given - * FlattenException. - * - * @author Fabien Potencier - * @author Matthias Pigulla - * - * @deprecated since Symfony 4.4, use Symfony\Component\HttpKernel\Controller\ErrorController instead. - */ -class ExceptionController -{ - protected $twig; - protected $debug; - - /** - * @param bool $debug Show error (false) or exception (true) pages by default - */ - public function __construct(Environment $twig, bool $debug) - { - $this->twig = $twig; - $this->debug = $debug; - } - - /** - * Converts an Exception to a Response. - * - * A "showException" request parameter can be used to force display of an error page (when set to false) or - * the exception page (when true). If it is not present, the "debug" value passed into the constructor will - * be used. - * - * @return Response - * - * @throws \InvalidArgumentException When the exception template does not exist - */ - public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null) - { - $currentContent = $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1)); - $showException = $request->attributes->get('showException', $this->debug); // As opposed to an additional parameter, this maintains BC - - $code = $exception->getStatusCode(); - - return new Response($this->twig->render( - $this->findTemplate($request, $request->getRequestFormat(), $code, $showException), - [ - 'status_code' => $code, - 'status_text' => Response::$statusTexts[$code] ?? '', - 'exception' => $exception, - 'logger' => $logger, - 'currentContent' => $currentContent, - ] - ), 200, ['Content-Type' => $request->getMimeType($request->getRequestFormat()) ?: 'text/html']); - } - - /** - * @param int $startObLevel - * - * @return string - */ - protected function getAndCleanOutputBuffering($startObLevel) - { - if (ob_get_level() <= $startObLevel) { - return ''; - } - - Response::closeOutputBuffers($startObLevel + 1, true); - - return ob_get_clean(); - } - - /** - * @param string $format - * @param int $code An HTTP response status code - * @param bool $showException - * - * @return string - */ - protected function findTemplate(Request $request, $format, $code, $showException) - { - $name = $showException ? 'exception' : 'error'; - if ($showException && 'html' == $format) { - $name = 'exception_full'; - } - - // For error pages, try to find a template for the specific HTTP status code and format - if (!$showException) { - $template = sprintf('@Twig/Exception/%s%s.%s.twig', $name, $code, $format); - if ($this->templateExists($template)) { - return $template; - } - } - - // try to find a template for the given format - $template = sprintf('@Twig/Exception/%s.%s.twig', $name, $format); - if ($this->templateExists($template)) { - return $template; - } - - // default to a generic HTML exception - $request->setRequestFormat('html'); - - return sprintf('@Twig/Exception/%s.html.twig', $showException ? 'exception_full' : $name); - } - - // to be removed when the minimum required version of Twig is >= 2.0 - protected function templateExists($template) - { - $template = (string) $template; - - $loader = $this->twig->getLoader(); - - if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { - try { - if ($loader instanceof SourceContextLoaderInterface) { - $loader->getSourceContext($template); - } else { - $loader->getSource($template); - } - - return true; - } catch (LoaderError $e) { - } - - return false; - } - - return $loader->exists($template); - } -} diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php deleted file mode 100644 index 23ac0b8704d49..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\Controller; - -use Symfony\Component\Debug\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\HttpKernelInterface; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use the "%s" instead.', PreviewErrorController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), \E_USER_DEPRECATED); - -/** - * PreviewErrorController can be used to test error pages. - * - * It will create a test exception and forward it to another controller. - * - * @author Matthias Pigulla - * - * @deprecated since Symfony 4.4, use the Symfony\Component\HttpKernel\Controller\ErrorController instead. - */ -class PreviewErrorController -{ - protected $kernel; - protected $controller; - - public function __construct(HttpKernelInterface $kernel, $controller) - { - $this->kernel = $kernel; - $this->controller = $controller; - } - - public function previewErrorPageAction(Request $request, $code) - { - $exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code); - - /* - * This Request mimics the parameters set by - * \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with - * the additional "showException" flag. - */ - - $subRequest = $request->duplicate(null, null, [ - '_controller' => $this->controller, - 'exception' => $exception, - 'logger' => null, - 'format' => $request->getRequestFormat(), - 'showException' => false, - ]); - - return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); - } -} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/CompatibilityServiceSubscriberInterface.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/CompatibilityServiceSubscriberInterface.php deleted file mode 100644 index 967f732ff59f9..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/CompatibilityServiceSubscriberInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\DependencyInjection; - -use Symfony\Component\DependencyInjection\ServiceSubscriberInterface as LegacyServiceSubscriberInterface; -use Symfony\Contracts\Service\ServiceSubscriberInterface; - -if (interface_exists(LegacyServiceSubscriberInterface::class)) { - /** - * @internal - */ - interface CompatibilityServiceSubscriberInterface extends LegacyServiceSubscriberInterface - { - } -} else { - /** - * @internal - */ - interface CompatibilityServiceSubscriberInterface extends ServiceSubscriberInterface - { - } -} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php deleted file mode 100644 index c21507dca8f1e..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler; - -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * Registers the Twig exception listener if Twig is registered as a templating engine. - * - * @author Fabien Potencier - * - * @internal - */ -class ExceptionListenerPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container) - { - if (false === $container->hasDefinition('twig')) { - return; - } - - // to be removed in 5.0 - // register the exception listener only if it's currently used, else use the provided by FrameworkBundle - if (null === $container->getParameter('twig.exception_listener.controller') && $container->hasDefinition('exception_listener')) { - $container->removeDefinition('twig.exception_listener'); - - return; - } - - if ($container->hasParameter('templating.engines')) { - $engines = $container->getParameter('templating.engines'); - if (\in_array('twig', $engines, true)) { - $container->removeDefinition('exception_listener'); - - return; - } - } - - $container->removeDefinition('twig.exception_listener'); - } -} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index de65dd5711918..12724e0f1cc65 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -11,12 +11,14 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler; -use Symfony\Bridge\Twig\Extension\AssetExtension; +use Symfony\Component\Asset\Packages; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Workflow\Workflow; +use Symfony\Component\Yaml\Yaml; /** * @author Jean-François Simon @@ -25,30 +27,25 @@ class ExtensionPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { - if (!class_exists(\Symfony\Component\Asset\Packages::class)) { + if (!class_exists(Packages::class)) { $container->removeDefinition('twig.extension.assets'); } - if (!class_exists(\Symfony\Component\ExpressionLanguage\Expression::class)) { + if (!class_exists(Expression::class)) { $container->removeDefinition('twig.extension.expression'); } - if (!interface_exists(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class)) { + if (!interface_exists(UrlGeneratorInterface::class)) { $container->removeDefinition('twig.extension.routing'); } - if (!class_exists(\Symfony\Component\Yaml\Yaml::class)) { + if (!class_exists(Yaml::class)) { $container->removeDefinition('twig.extension.yaml'); } $viewDir = \dirname((new \ReflectionClass(\Symfony\Bridge\Twig\Extension\FormExtension::class))->getFileName(), 2).'/Resources/views'; $templateIterator = $container->getDefinition('twig.template_iterator'); - $templatePaths = $templateIterator->getArgument(2); - $cacheWarmer = null; - if ($container->hasDefinition('twig.cache_warmer')) { - $cacheWarmer = $container->getDefinition('twig.cache_warmer'); - $cacheWarmerPaths = $cacheWarmer->getArgument(2); - } + $templatePaths = $templateIterator->getArgument(1); $loader = $container->getDefinition('twig.loader.native_filesystem'); if ($container->has('mailer')) { @@ -56,9 +53,6 @@ public function process(ContainerBuilder $container) $loader->addMethodCall('addPath', [$emailPath, 'email']); $loader->addMethodCall('addPath', [$emailPath, '!email']); $templatePaths[$emailPath] = 'email'; - if ($cacheWarmer) { - $cacheWarmerPaths[$emailPath] = 'email'; - } } if ($container->has('form.extension')) { @@ -67,15 +61,9 @@ public function process(ContainerBuilder $container) $coreThemePath = $viewDir.'/Form'; $loader->addMethodCall('addPath', [$coreThemePath]); $templatePaths[$coreThemePath] = null; - if ($cacheWarmer) { - $cacheWarmerPaths[$coreThemePath] = null; - } } - $templateIterator->replaceArgument(2, $templatePaths); - if ($cacheWarmer) { - $container->getDefinition('twig.cache_warmer')->replaceArgument(2, $cacheWarmerPaths); - } + $templateIterator->replaceArgument(1, $templatePaths); if ($container->has('router')) { $container->getDefinition('twig.extension.routing')->addTag('twig.extension'); @@ -85,19 +73,13 @@ public function process(ContainerBuilder $container) $container->getDefinition('twig.extension.httpkernel')->addTag('twig.extension'); $container->getDefinition('twig.runtime.httpkernel')->addTag('twig.runtime'); - // inject Twig in the hinclude service if Twig is the only registered templating engine - if ((!$container->hasParameter('templating.engines') || ['twig'] == $container->getParameter('templating.engines')) && $container->hasDefinition('fragment.renderer.hinclude')) { + if ($container->hasDefinition('fragment.renderer.hinclude')) { $container->getDefinition('fragment.renderer.hinclude') ->addTag('kernel.fragment_renderer', ['alias' => 'hinclude']) - ->replaceArgument(0, new Reference('twig')) ; } } - if (!$container->has('http_kernel')) { - $container->removeDefinition('twig.controller.preview_error'); - } - if ($container->has('request_stack')) { $container->getDefinition('twig.extension.httpfoundation')->addTag('twig.extension'); } @@ -115,21 +97,7 @@ public function process(ContainerBuilder $container) $container->getDefinition('twig.extension.weblink')->addTag('twig.extension'); } - $twigLoader = $container->getDefinition('twig.loader.native_filesystem'); - if ($container->has('templating')) { - $loader = $container->getDefinition('twig.loader.filesystem'); - $loader->setMethodCalls(array_merge($twigLoader->getMethodCalls(), $loader->getMethodCalls())); - - if (!method_exists(AssetExtension::class, 'getName')) { - $container->removeDefinition('templating.engine.twig'); - } - - $twigLoader->clearTag('twig.loader'); - } else { - $container->setAlias('twig.loader.filesystem', new Alias('twig.loader.native_filesystem', false)); - $container->removeDefinition('templating.engine.twig'); - $container->removeDefinition('twig.cache_warmer'); - } + $container->setAlias('twig.loader.filesystem', new Alias('twig.loader.native_filesystem', false)); if ($container->has('assets.packages')) { $container->getDefinition('twig.extension.assets')->addTag('twig.extension'); @@ -152,5 +120,10 @@ public function process(ContainerBuilder $container) } else { $container->getDefinition('workflow.twig_extension')->addTag('twig.extension'); } + + if ($container->has('serializer')) { + $container->getDefinition('twig.runtime.serializer')->addTag('twig.runtime'); + $container->getDefinition('twig.extension.serializer')->addTag('twig.extension'); + } } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php index 9b1345d4c33f0..a422f668257ad 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php @@ -43,7 +43,7 @@ public function process(ContainerBuilder $container) } if (1 === $found) { - $container->setAlias('twig.loader', $id)->setPrivate(true); + $container->setAlias('twig.loader', $id); } else { $chainLoader = $container->getDefinition('twig.loader.chain'); krsort($prioritizedLoaders); @@ -54,7 +54,7 @@ public function process(ContainerBuilder $container) } } - $container->setAlias('twig.loader', 'twig.loader.chain')->setPrivate(true); + $container->setAlias('twig.loader', 'twig.loader.chain'); } } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index 3ec4bc3f1e3c5..df688c5dd43ee 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; /** * TwigExtension configuration structure. @@ -24,33 +25,24 @@ class Configuration implements ConfigurationInterface { /** * Generates the configuration tree builder. - * - * @return TreeBuilder The tree builder */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('twig'); $rootNode = $treeBuilder->getRootNode(); - $rootNode - ->children() - ->scalarNode('exception_controller') - ->defaultValue(static function () { - @trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', \E_USER_DEPRECATED); + $rootNode->beforeNormalization() + ->ifTrue(function ($v) { return \is_array($v) && \array_key_exists('exception_controller', $v); }) + ->then(function ($v) { + if (isset($v['exception_controller'])) { + throw new InvalidConfigurationException('Option "exception_controller" under "twig" must be null or unset, use "error_controller" under "framework" instead.'); + } - return 'twig.controller.exception::showAction'; - }) - ->validate() - ->ifTrue(static function ($v) { return null !== $v; }) - ->then(static function ($v) { - @trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', \E_USER_DEPRECATED); + unset($v['exception_controller']); - return $v; - }) - ->end() - ->end() - ->end() - ; + return $v; + }) + ->end(); $this->addFormThemesSection($rootNode); $this->addGlobalsSection($rootNode); @@ -142,13 +134,7 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode) ->scalarNode('cache')->defaultValue('%kernel.cache_dir%/twig')->end() ->scalarNode('charset')->defaultValue('%kernel.charset%')->end() ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() - ->booleanNode('strict_variables') - ->defaultValue(function () { - @trigger_error('Relying on the default value ("false") of the "twig.strict_variables" configuration option is deprecated since Symfony 4.1. You should use "%kernel.debug%" explicitly instead, which will be the new default in 5.0.', \E_USER_DEPRECATED); - - return false; - }) - ->end() + ->booleanNode('strict_variables')->defaultValue('%kernel.debug%')->end() ->scalarNode('auto_reload')->end() ->integerNode('optimizations')->min(-1)->end() ->scalarNode('default_path') diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php index 07ec691769ec7..778d19f523942 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php @@ -24,12 +24,12 @@ class_exists(Environment::class); */ class EnvironmentConfigurator { - private $dateFormat; - private $intervalFormat; - private $timezone; - private $decimals; - private $decimalPoint; - private $thousandsSeparator; + private string $dateFormat; + private string $intervalFormat; + private ?string $timezone; + private int $decimals; + private string $decimalPoint; + private string $thousandsSeparator; public function __construct(string $dateFormat, string $intervalFormat, ?string $timezone, int $decimals, string $decimalPoint, string $thousandsSeparator) { diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index 234a845edadd6..2fb47d3746aef 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -11,13 +11,13 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection; -use Symfony\Bundle\TwigBundle\Loader\NativeFilesystemLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Form\Form; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Translation\Translator; @@ -35,26 +35,22 @@ class TwigExtension extends Extension { public function load(array $configs, ContainerBuilder $container) { - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('twig.xml'); + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('twig.php'); - if (class_exists(\Symfony\Component\Form\Form::class)) { - $loader->load('form.xml'); + if ($container::willBeAvailable('symfony/form', Form::class, ['symfony/twig-bundle'])) { + $loader->load('form.php'); } - if (interface_exists(\Symfony\Component\Templating\EngineInterface::class)) { - $loader->load('templating.xml'); + if ($container::willBeAvailable('symfony/console', Application::class, ['symfony/twig-bundle'])) { + $loader->load('console.php'); } - if (class_exists(Application::class)) { - $loader->load('console.xml'); + if ($container::willBeAvailable('symfony/mailer', Mailer::class, ['symfony/twig-bundle'])) { + $loader->load('mailer.php'); } - if (class_exists(Mailer::class)) { - $loader->load('mailer.xml'); - } - - if (!class_exists(Translator::class)) { + if (!$container::willBeAvailable('symfony/translation', Translator::class, ['symfony/twig-bundle'])) { $container->removeDefinition('twig.translation.extractor'); } @@ -75,8 +71,6 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->processConfiguration($configuration, $configs); - $container->setParameter('twig.exception_listener.controller', $config['exception_controller']); - $container->setParameter('twig.form.resources', $config['form_themes']); $container->setParameter('twig.default_path', $config['default_path']); $defaultTwigPath = $container->getParameterBag()->resolveValue($config['default_path']); @@ -91,10 +85,6 @@ public function load(array $configs, ContainerBuilder $container) $twigFilesystemLoaderDefinition = $container->getDefinition('twig.loader.native_filesystem'); - if ($container->getParameter('kernel.debug')) { - $twigFilesystemLoaderDefinition->setClass(NativeFilesystemLoader::class); - } - // register user-configured paths foreach ($config['paths'] as $path => $namespace) { if (!$namespace) { @@ -105,8 +95,7 @@ public function load(array $configs, ContainerBuilder $container) } // paths are modified in ExtensionPass if forms are enabled - $container->getDefinition('twig.cache_warmer')->replaceArgument(2, $config['paths']); - $container->getDefinition('twig.template_iterator')->replaceArgument(2, $config['paths']); + $container->getDefinition('twig.template_iterator')->replaceArgument(1, $config['paths']); foreach ($this->getBundleTemplatePaths($container, $config) as $name => $paths) { $namespace = $this->normalizeBundleName($name); @@ -120,15 +109,6 @@ public function load(array $configs, ContainerBuilder $container) } } - if (file_exists($dir = $container->getParameter('kernel.root_dir').'/Resources/views')) { - if ($dir !== $defaultTwigPath) { - @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultTwigPath), \E_USER_DEPRECATED); - } - - $twigFilesystemLoaderDefinition->addMethodCall('addPath', [$dir]); - } - $container->addResource(new FileExistenceResource($dir)); - if (file_exists($defaultTwigPath)) { $twigFilesystemLoaderDefinition->addMethodCall('addPath', [$defaultTwigPath]); } @@ -167,7 +147,6 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(RuntimeExtensionInterface::class)->addTag('twig.runtime'); if (false === $config['cache']) { - $container->removeDefinition('twig.cache_warmer'); $container->removeDefinition('twig.template_cache_warmer'); } } @@ -178,13 +157,6 @@ private function getBundleTemplatePaths(ContainerBuilder $container, array $conf foreach ($container->getParameter('kernel.bundles_metadata') as $name => $bundle) { $defaultOverrideBundlePath = $container->getParameterBag()->resolveValue($config['default_path']).'/bundles/'.$name; - if (file_exists($dir = $container->getParameter('kernel.root_dir').'/Resources/'.$name.'/views')) { - @trigger_error(sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $name, $dir, $defaultOverrideBundlePath), \E_USER_DEPRECATED); - - $bundleHierarchy[$name][] = $dir; - } - $container->addResource(new FileExistenceResource($dir)); - if (file_exists($defaultOverrideBundlePath)) { $bundleHierarchy[$name][] = $defaultOverrideBundlePath; } @@ -211,12 +183,12 @@ private function normalizeBundleName(string $name): string /** * {@inheritdoc} */ - public function getXsdValidationBasePath() + public function getXsdValidationBasePath(): string|false { return __DIR__.'/../Resources/config/schema'; } - public function getNamespace() + public function getNamespace(): string { return 'http://symfony.com/schema/dic/twig'; } diff --git a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php deleted file mode 100644 index 19fd158dc96ff..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\Loader; - -@trigger_error('The '.FilesystemLoader::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig notation for templates instead.', \E_USER_DEPRECATED); - -use Symfony\Component\Config\FileLocatorInterface; -use Symfony\Component\Templating\TemplateNameParserInterface; -use Symfony\Component\Templating\TemplateReferenceInterface; -use Twig\Error\LoaderError; -use Twig\Loader\FilesystemLoader as BaseFilesystemLoader; - -/** - * FilesystemLoader extends the default Twig filesystem loader - * to work with the Symfony paths and template references. - * - * @author Fabien Potencier - * - * @deprecated since version 4.3, to be removed in 5.0; use Twig notation for templates instead. - */ -class FilesystemLoader extends BaseFilesystemLoader -{ - protected $locator; - protected $parser; - - /** - * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) - */ - public function __construct(FileLocatorInterface $locator, TemplateNameParserInterface $parser, string $rootPath = null) - { - parent::__construct([], $rootPath); - - $this->locator = $locator; - $this->parser = $parser; - } - - /** - * {@inheritdoc} - * - * The name parameter might also be a TemplateReferenceInterface. - * - * @return bool - */ - public function exists($name) - { - return parent::exists((string) $name); - } - - /** - * Returns the path to the template file. - * - * The file locator is used to locate the template when the naming convention - * is the symfony one (i.e. the name can be parsed). - * Otherwise the template is located using the locator from the twig library. - * - * @param string|TemplateReferenceInterface $template The template - * @param bool $throw When true, a LoaderError exception will be thrown if a template could not be found - * - * @return string The path to the template file - * - * @throws LoaderError if the template could not be found - */ - protected function findTemplate($template, $throw = true) - { - $logicalName = (string) $template; - - if (isset($this->cache[$logicalName])) { - return $this->cache[$logicalName]; - } - - $file = null; - try { - $file = parent::findTemplate($logicalName); - } catch (LoaderError $e) { - $twigLoaderException = $e; - - // for BC - try { - $template = $this->parser->parse($template); - $file = $this->locator->locate($template); - } catch (\Exception $e) { - } - } - - if (false === $file || null === $file) { - if ($throw) { - throw $twigLoaderException; - } - - return null; - } - - return $this->cache[$logicalName] = $file; - } -} diff --git a/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php deleted file mode 100644 index 493fe250779df..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\TwigBundle\Loader; - -use Twig\Error\LoaderError; -use Twig\Loader\FilesystemLoader; - -/** - * @author Behnoush Norouzali - * - * @internal - */ -class NativeFilesystemLoader extends FilesystemLoader -{ - /** - * {@inheritdoc} - * - * @return string|null - */ - protected function findTemplate($template, $throw = true) - { - try { - return parent::findTemplate($template, $throw); - } catch (LoaderError $e) { - if ('' === $template || '@' === $template[0] || !preg_match('/^(?P[^:]*?)(?:Bundle)?:(?P[^:]*+):(?P