diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a1fb40d5..1cd6b1bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ CHANGELOG ========= +7.4 +--- + + * Auto-generate `config/reference.php` to assist in writing and discovering app's configuration + * Auto-register routes from attributes found on controller services + * Add `ControllerHelper`; the helpers from AbstractController as a standalone service + * Allow using their name without added suffix when using `#[Target]` for custom services + * Deprecate `Symfony\Bundle\FrameworkBundle\Console\Application::add()` in favor of `Symfony\Bundle\FrameworkBundle\Console\Application::addCommand()` + * Add `assertEmailAddressNotContains()` to the `MailerAssertionsTrait` + * Add `framework.type_info.aliases` option + * Add `KernelBrowser::getSession()` + * Add support for configuring workflow places with glob patterns matching consts/backed enums + * Add support for configuring the `CachingHttpClient` + * Add support for weighted transitions in workflows + * Add support for union types with `Symfony\Component\EventDispatcher\Attribute\AsEventListener` + * Add `framework.allowed_http_method_override` option + * Initialize `router.request_context`'s `_locale` parameter to `%kernel.default_locale%` + * Deprecate `ConfigBuilderCacheWarmer`, return PHP arrays from your config instead + 7.3 --- @@ -699,7 +718,7 @@ CHANGELOG * added Client::enableProfiler() * a new parameter has been added to the DIC: `router.request_context.base_url` You can customize it for your functional tests or for generating URLs with - the right base URL when your are in the CLI context. + the right base URL when you are in the CLI context. * added support for default templates per render tag 2.1.0 diff --git a/CacheWarmer/ConfigBuilderCacheWarmer.php b/CacheWarmer/ConfigBuilderCacheWarmer.php index 48ed51aec..6f29a7b74 100644 --- a/CacheWarmer/ConfigBuilderCacheWarmer.php +++ b/CacheWarmer/ConfigBuilderCacheWarmer.php @@ -30,7 +30,7 @@ * * @author Tobias Nyholm * - * @final since Symfony 7.1 + * @deprecated since Symfony 7.4 */ class ConfigBuilderCacheWarmer implements CacheWarmerInterface { diff --git a/CacheWarmer/SerializerCacheWarmer.php b/CacheWarmer/SerializerCacheWarmer.php index fbf7083b7..ae6aa430b 100644 --- a/CacheWarmer/SerializerCacheWarmer.php +++ b/CacheWarmer/SerializerCacheWarmer.php @@ -14,13 +14,14 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; /** - * Warms up XML and YAML serializer metadata. + * Warms up serializer metadata. * * @author Titouan Galopin * @@ -66,14 +67,14 @@ protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?strin /** * @param LoaderInterface[] $loaders * - * @return XmlFileLoader[]|YamlFileLoader[] + * @return list */ private function extractSupportedLoaders(array $loaders): array { $supportedLoaders = []; foreach ($loaders as $loader) { - if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) { + if (method_exists($loader, 'getMappedClasses')) { $supportedLoaders[] = $loader; } elseif ($loader instanceof LoaderChain) { $supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders())); diff --git a/CacheWarmer/ValidatorCacheWarmer.php b/CacheWarmer/ValidatorCacheWarmer.php index 9c313f80a..c167d1415 100644 --- a/CacheWarmer/ValidatorCacheWarmer.php +++ b/CacheWarmer/ValidatorCacheWarmer.php @@ -14,6 +14,7 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; @@ -21,7 +22,7 @@ use Symfony\Component\Validator\ValidatorBuilder; /** - * Warms up XML and YAML validator metadata. + * Warms up validator metadata. * * @author Titouan Galopin * @@ -77,14 +78,14 @@ protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array /** * @param LoaderInterface[] $loaders * - * @return XmlFileLoader[]|YamlFileLoader[] + * @return list */ private function extractSupportedLoaders(array $loaders): array { $supportedLoaders = []; foreach ($loaders as $loader) { - if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) { + if (method_exists($loader, 'getMappedClasses')) { $supportedLoaders[] = $loader; } elseif ($loader instanceof LoaderChain) { $supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders())); diff --git a/Command/AboutCommand.php b/Command/AboutCommand.php index 0c6899328..8b4604970 100644 --- a/Command/AboutCommand.php +++ b/Command/AboutCommand.php @@ -36,11 +36,11 @@ protected function configure(): void { $this ->setHelp(<<<'EOT' -The %command.name% command displays information about the current Symfony project. + 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. -EOT + The PHP section displays important configuration that could affect your application. The values might + be different between web and CLI. + EOT ) ; } diff --git a/Command/AssetsInstallCommand.php b/Command/AssetsInstallCommand.php index 5dc8c828e..099204cae 100644 --- a/Command/AssetsInstallCommand.php +++ b/Command/AssetsInstallCommand.php @@ -23,7 +23,6 @@ use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; -use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; /** @@ -58,24 +57,24 @@ protected function configure(): void ->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') ->setHelp(<<<'EOT' -The %command.name% command installs bundle assets into a given -directory (e.g. the public directory). + The %command.name% command installs bundle assets into a given + directory (e.g. the public directory). - php %command.full_name% public + php %command.full_name% public -A "bundles" directory will be created inside the target directory and the -"Resources/public" directory of each bundle will be copied into it. + A "bundles" directory will be created inside the target directory and the + "Resources/public" directory of each bundle will be copied into it. -To create a symlink to each bundle instead of copying its assets, use the ---symlink option (will fall back to hard copies when symbolic links aren't possible: + To create a symlink to each bundle instead of copying its assets, use the + --symlink option (will fall back to hard copies when symbolic links aren't possible: - php %command.full_name% public --symlink + php %command.full_name% public --symlink -To make symlink relative, add the --relative option: + To make symlink relative, add the --relative option: - php %command.full_name% public --symlink --relative + php %command.full_name% public --symlink --relative -EOT + EOT ) ; } @@ -119,7 +118,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $copyUsed = false; $exitCode = 0; $validAssetDirs = []; - /** @var BundleInterface $bundle */ foreach ($kernel->getBundles() as $bundle) { if (!is_dir($originDir = $bundle->getPath().'/Resources/public') && !is_dir($originDir = $bundle->getPath().'/public')) { continue; @@ -239,7 +237,7 @@ private function symlink(string $originDir, string $targetDir, bool $relative = */ private function hardCopy(string $originDir, string $targetDir): string { - $this->filesystem->mkdir($targetDir, 0777); + $this->filesystem->mkdir($targetDir, 0o777); // We use a custom iterator to ignore VCS files $this->filesystem->mirror($originDir, $targetDir, Finder::create()->ignoreDotFiles(false)->in($originDir)); diff --git a/Command/BuildDebugContainerTrait.php b/Command/BuildDebugContainerTrait.php index 2f625e9e3..011510095 100644 --- a/Command/BuildDebugContainerTrait.php +++ b/Command/BuildDebugContainerTrait.php @@ -39,7 +39,9 @@ protected function getContainerBuilder(KernelInterface $kernel): ContainerBuilde return $this->container; } - if (!$kernel->isDebug() || !$kernel->getContainer()->getParameter('debug.container.dump') || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { + $file = $kernel->isDebug() ? $kernel->getContainer()->getParameter('debug.container.dump') : false; + + if (!$file || !(new ConfigCache($file, true))->isFresh()) { $buildContainer = \Closure::bind(function () { $this->initializeBundles(); @@ -57,13 +59,17 @@ protected function getContainerBuilder(KernelInterface $kernel): ContainerBuilde return $containerBuilder; }, $kernel, $kernel::class); $container = $buildContainer(); - (new XmlFileLoader($container, new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); - $locatorPass = new ServiceLocatorTagPass(); - $locatorPass->process($container); - $container->getCompilerPassConfig()->setBeforeOptimizationPasses([]); - $container->getCompilerPassConfig()->setOptimizationPasses([]); - $container->getCompilerPassConfig()->setBeforeRemovingPasses([]); + if (str_ends_with($file, '.xml') && is_file(substr_replace($file, '.ser', -4))) { + $dumpedContainer = unserialize(file_get_contents(substr_replace($file, '.ser', -4))); + $container->setDefinitions($dumpedContainer->getDefinitions()); + $container->setAliases($dumpedContainer->getAliases()); + $container->__construct($dumpedContainer->getParameterBag()); + } else { + (new XmlFileLoader($container, new FileLocator()))->load($file); + $locatorPass = new ServiceLocatorTagPass(); + $locatorPass->process($container); + } } return $this->container = $container; diff --git a/Command/CacheClearCommand.php b/Command/CacheClearCommand.php index 0e48ead59..3f3960ef8 100644 --- a/Command/CacheClearCommand.php +++ b/Command/CacheClearCommand.php @@ -56,12 +56,12 @@ protected function configure(): void new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'), ]) ->setHelp(<<<'EOF' -The %command.name% command clears and warms up the application cache for a given environment -and debug mode: + The %command.name% command clears and warms up the application cache for a given environment + and debug mode: - php %command.full_name% --env=dev - php %command.full_name% --env=prod --no-debug -EOF + php %command.full_name% --env=dev + php %command.full_name% --env=prod --no-debug + EOF ) ; } @@ -213,7 +213,7 @@ private function isNfs(string $dir): bool if ('/' === \DIRECTORY_SEPARATOR && @is_readable('/proc/mounts') && $files = @file('/proc/mounts')) { foreach ($files as $mount) { $mount = \array_slice(explode(' ', $mount), 1, -3); - if (!\in_array(array_pop($mount), ['vboxsf', 'nfs'])) { + if (!\in_array(array_pop($mount), ['vboxsf', 'nfs'], true)) { continue; } $mounts[] = implode(' ', $mount).'/'; diff --git a/Command/CachePoolClearCommand.php b/Command/CachePoolClearCommand.php index 5d840e597..d4bca0d8f 100644 --- a/Command/CachePoolClearCommand.php +++ b/Command/CachePoolClearCommand.php @@ -51,10 +51,10 @@ protected function configure(): void ->addOption('all', null, InputOption::VALUE_NONE, 'Clear all cache pools') ->addOption('exclude', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'A list of cache pools or cache pool clearers to exclude') ->setHelp(<<<'EOF' -The %command.name% command clears the given cache pools or cache pool clearers. + The %command.name% command clears the given cache pools or cache pool clearers. - %command.full_name% [...] -EOF + %command.full_name% [...] + EOF ) ; } diff --git a/Command/CachePoolDeleteCommand.php b/Command/CachePoolDeleteCommand.php index 8fb1d1aaa..c3c23a391 100644 --- a/Command/CachePoolDeleteCommand.php +++ b/Command/CachePoolDeleteCommand.php @@ -47,10 +47,10 @@ protected function configure(): void new InputArgument('key', InputArgument::REQUIRED, 'The cache key to delete from the pool'), ]) ->setHelp(<<<'EOF' -The %command.name% deletes an item from a given cache pool. + The %command.name% deletes an item from a given cache pool. - %command.full_name% -EOF + %command.full_name% + EOF ) ; } diff --git a/Command/CachePoolListCommand.php b/Command/CachePoolListCommand.php index 6b8e71eb0..6aedfb0c0 100644 --- a/Command/CachePoolListCommand.php +++ b/Command/CachePoolListCommand.php @@ -38,8 +38,8 @@ protected function configure(): void { $this ->setHelp(<<<'EOF' -The %command.name% command lists all available cache pools. -EOF + The %command.name% command lists all available cache pools. + EOF ) ; } diff --git a/Command/CachePoolPruneCommand.php b/Command/CachePoolPruneCommand.php index 745a001cc..5036da8ef 100644 --- a/Command/CachePoolPruneCommand.php +++ b/Command/CachePoolPruneCommand.php @@ -39,10 +39,10 @@ protected function configure(): void { $this ->setHelp(<<<'EOF' -The %command.name% command deletes all expired items from all pruneable pools. + The %command.name% command deletes all expired items from all pruneable pools. - %command.full_name% -EOF + %command.full_name% + EOF ) ; } diff --git a/Command/CacheWarmupCommand.php b/Command/CacheWarmupCommand.php index cc9665eb1..6dd01447e 100644 --- a/Command/CacheWarmupCommand.php +++ b/Command/CacheWarmupCommand.php @@ -44,11 +44,11 @@ protected function configure(): void new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'), ]) ->setHelp(<<<'EOF' -The %command.name% command warms up the cache. + The %command.name% command warms up the cache. -Before running this command, the cache must be empty. + Before running this command, the cache must be empty. -EOF + EOF ) ; } diff --git a/Command/ConfigDebugCommand.php b/Command/ConfigDebugCommand.php index 8d5f85cee..50c8dddf5 100644 --- a/Command/ConfigDebugCommand.php +++ b/Command/ConfigDebugCommand.php @@ -49,23 +49,23 @@ protected function configure(): void new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), class_exists(Yaml::class) ? 'txt' : 'json'), ]) ->setHelp(<<%command.name% command dumps the current configuration for an -extension/bundle. + The %command.name% command dumps the current configuration for an + extension/bundle. -Either the extension alias or bundle name can be used: + Either the extension alias or bundle name can be used: - php %command.full_name% framework - php %command.full_name% FrameworkBundle + php %command.full_name% framework + php %command.full_name% FrameworkBundle -The --format option specifies the format of the command output: + The --format option specifies the format of the command output: - php %command.full_name% framework --format=json + php %command.full_name% framework --format=json -For dumping a specific option, add its path as second argument: + For dumping a specific option, add its path as second argument: - php %command.full_name% framework serializer.enabled + php %command.full_name% framework serializer.enabled -EOF + EOF ) ; } diff --git a/Command/ConfigDumpReferenceCommand.php b/Command/ConfigDumpReferenceCommand.php index 3cb744d74..934008061 100644 --- a/Command/ConfigDumpReferenceCommand.php +++ b/Command/ConfigDumpReferenceCommand.php @@ -47,23 +47,23 @@ protected function configure(): void new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'yaml'), ]) ->setHelp(<<%command.name% command dumps the default configuration for an -extension/bundle. + The %command.name% command dumps the default configuration for an + extension/bundle. -Either the extension alias or bundle name can be used: + Either the extension alias or bundle name can be used: - php %command.full_name% framework - php %command.full_name% FrameworkBundle + php %command.full_name% framework + php %command.full_name% FrameworkBundle -The --format option specifies the format of the command output: + The --format option specifies the format of the command output: - php %command.full_name% FrameworkBundle --format=json + php %command.full_name% FrameworkBundle --format=json -For dumping a specific option, add its path as second argument (only available for the yaml format): + For dumping a specific option, add its path as second argument (only available for the yaml format): - php %command.full_name% framework http_client.default_options + php %command.full_name% framework http_client.default_options -EOF + EOF ) ; } @@ -142,7 +142,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))); } - $io->writeln(null === $path ? $dumper->dump($configuration, $extension->getNamespace()) : $dumper->dumpAtPath($configuration, $path)); + $io->writeln(null === $path ? $dumper->dump($configuration, method_exists($extension, 'getNamespace') ? $extension->getNamespace() : null) : $dumper->dumpAtPath($configuration, $path)); return 0; } diff --git a/Command/ContainerDebugCommand.php b/Command/ContainerDebugCommand.php index 17c71bdca..f0f7d5a1b 100644 --- a/Command/ContainerDebugCommand.php +++ b/Command/ContainerDebugCommand.php @@ -57,59 +57,59 @@ protected function configure(): void new InputOption('deprecations', null, InputOption::VALUE_NONE, 'Display deprecations generated when compiling and warming up the container'), ]) ->setHelp(<<<'EOF' -The %command.name% command displays all configured public services: + The %command.name% command displays all configured public services: - php %command.full_name% + php %command.full_name% -To see deprecations generated during container compilation and cache warmup, use the --deprecations option: + To see deprecations generated during container compilation and cache warmup, use the --deprecations option: - php %command.full_name% --deprecations + php %command.full_name% --deprecations -To get specific information about a service, specify its name: + To get specific information about a service, specify its name: - php %command.full_name% validator + php %command.full_name% validator -To get specific information about a service including all its arguments, use the --show-arguments flag: + To get specific information about a service including all its arguments, use the --show-arguments flag: - php %command.full_name% validator --show-arguments + php %command.full_name% validator --show-arguments -To see available types that can be used for autowiring, use the --types flag: + To see available types that can be used for autowiring, use the --types flag: - php %command.full_name% --types + php %command.full_name% --types -To see environment variables used by the container, use the --env-vars flag: + To see environment variables used by the container, use the --env-vars flag: - php %command.full_name% --env-vars + php %command.full_name% --env-vars -Display a specific environment variable by specifying its name with the --env-var option: + Display a specific environment variable by specifying its name with the --env-var option: - php %command.full_name% --env-var=APP_ENV + php %command.full_name% --env-var=APP_ENV -Use the --tags option to display tagged public services grouped by tag: + Use the --tags option to display tagged public services grouped by tag: - php %command.full_name% --tags + php %command.full_name% --tags -Find all services with a specific tag by specifying the tag name with the --tag option: + Find all services with a specific tag by specifying the tag name with the --tag option: - php %command.full_name% --tag=form.type + php %command.full_name% --tag=form.type -Use the --parameters option to display all parameters: + Use the --parameters option to display all parameters: - php %command.full_name% --parameters + php %command.full_name% --parameters -Display a specific parameter by specifying its name with the --parameter option: + Display a specific parameter by specifying its name with the --parameter option: - php %command.full_name% --parameter=kernel.debug + php %command.full_name% --parameter=kernel.debug -By default, internal services are hidden. You can display them -using the --show-hidden flag: + By default, internal services are hidden. You can display them + using the --show-hidden flag: - php %command.full_name% --show-hidden + php %command.full_name% --show-hidden -The --format option specifies the format of the command output: + The --format option specifies the format of the command output: - php %command.full_name% --format=json -EOF + php %command.full_name% --format=json + EOF ) ; } diff --git a/Command/ContainerLintCommand.php b/Command/ContainerLintCommand.php index d71fd6810..2fc0be7c5 100644 --- a/Command/ContainerLintCommand.php +++ b/Command/ContainerLintCommand.php @@ -25,7 +25,6 @@ use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\ResolveFactoryClassPass; use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass; -use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -82,9 +81,10 @@ private function getContainerBuilder(bool $resolveEnvVars): ContainerBuilder } $kernel = $this->getApplication()->getKernel(); - $kernelContainer = $kernel->getContainer(); + $container = $kernel->getContainer(); + $file = $kernel->isDebug() ? $container->getParameter('debug.container.dump') : false; - if (!$kernel->isDebug() || !$kernelContainer->getParameter('debug.container.dump') || !(new ConfigCache($kernelContainer->getParameter('debug.container.dump'), true))->isFresh()) { + if (!$file || !(new ConfigCache($file, true))->isFresh()) { if (!$kernel instanceof Kernel) { throw new RuntimeException(\sprintf('This command does not support the application kernel: "%s" does not extend "%s".', get_debug_type($kernel), Kernel::class)); } @@ -96,15 +96,20 @@ private function getContainerBuilder(bool $resolveEnvVars): ContainerBuilder }, $kernel, $kernel::class); $container = $buildContainer(); } else { - if (!$kernelContainer instanceof Container) { - throw new RuntimeException(\sprintf('This command does not support the application container: "%s" does not extend "%s".', get_debug_type($kernelContainer), Container::class)); + if (str_ends_with($file, '.xml') && is_file(substr_replace($file, '.ser', -4))) { + $container = unserialize(file_get_contents(substr_replace($file, '.ser', -4))); + } else { + (new XmlFileLoader($container = new ContainerBuilder(new EnvPlaceholderParameterBag()), new FileLocator()))->load($file); } - (new XmlFileLoader($container = new ContainerBuilder($parameterBag = new EnvPlaceholderParameterBag()), new FileLocator()))->load($kernelContainer->getParameter('debug.container.dump')); + if (!$container instanceof ContainerBuilder) { + throw new RuntimeException(\sprintf('This command does not support the application container: "%s" is not a "%s".', get_debug_type($container), ContainerBuilder::class)); + } if ($resolveEnvVars) { $container->getCompilerPassConfig()->setOptimizationPasses([new ResolveParameterPlaceHoldersPass(), new ResolveFactoryClassPass()]); } else { + $parameterBag = $container->getParameterBag(); $refl = new \ReflectionProperty($parameterBag, 'resolved'); $refl->setValue($parameterBag, true); diff --git a/Command/DebugAutowiringCommand.php b/Command/DebugAutowiringCommand.php index e159c5a39..5c1869c6a 100644 --- a/Command/DebugAutowiringCommand.php +++ b/Command/DebugAutowiringCommand.php @@ -20,7 +20,6 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; /** @@ -48,16 +47,16 @@ protected function configure(): void new InputOption('all', null, InputOption::VALUE_NONE, 'Show also services that are not aliased'), ]) ->setHelp(<<<'EOF' -The %command.name% command displays the classes and interfaces that -you can use as type-hints for autowiring: + The %command.name% command displays the classes and interfaces that + you can use as type-hints for autowiring: - php %command.full_name% + php %command.full_name% -You can also pass a search term to filter the list: + You can also pass a search term to filter the list: - php %command.full_name% log + php %command.full_name% log -EOF + EOF ) ; } @@ -137,7 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $target = substr($id, \strlen($previousId) + 3); - if ($previousId.' $'.(new Target($target))->getParsedName() === $serviceId) { + if ($container->findDefinition($id) === $container->findDefinition($serviceId)) { $serviceLine .= ' - target:'.$target.''; break; } @@ -185,7 +184,7 @@ private function getFileLink(string $class): string return ''; } - return (string) $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine()); + return $r->getFileName() ? ($this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine()) ?: '') : ''; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void diff --git a/Command/EventDispatcherDebugCommand.php b/Command/EventDispatcherDebugCommand.php index 3c51cb1b7..43766ed92 100644 --- a/Command/EventDispatcherDebugCommand.php +++ b/Command/EventDispatcherDebugCommand.php @@ -53,18 +53,18 @@ protected function configure(): void new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), ]) ->setHelp(<<<'EOF' -The %command.name% command displays all configured listeners: + The %command.name% command displays all configured listeners: - php %command.full_name% + php %command.full_name% -To get specific listeners for an event, specify its name: + To get specific listeners for an event, specify its name: - php %command.full_name% kernel.request + php %command.full_name% kernel.request -The --format option specifies the format of the command output: + The --format option specifies the format of the command output: - php %command.full_name% --format=json -EOF + php %command.full_name% --format=json + EOF ) ; } diff --git a/Command/RouterDebugCommand.php b/Command/RouterDebugCommand.php index e54377115..3daf865b3 100644 --- a/Command/RouterDebugCommand.php +++ b/Command/RouterDebugCommand.php @@ -58,14 +58,14 @@ protected function configure(): void new InputOption('method', null, InputOption::VALUE_REQUIRED, 'Filter by HTTP method', '', ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']), ]) ->setHelp(<<<'EOF' -The %command.name% displays the configured routes: + The %command.name% displays the configured routes: - php %command.full_name% + php %command.full_name% -The --format option specifies the format of the command output: + The --format option specifies the format of the command output: - php %command.full_name% --format=json -EOF + php %command.full_name% --format=json + EOF ) ; } diff --git a/Command/RouterMatchCommand.php b/Command/RouterMatchCommand.php index 3f0ea3cb5..dee448517 100644 --- a/Command/RouterMatchCommand.php +++ b/Command/RouterMatchCommand.php @@ -53,15 +53,15 @@ protected function configure(): void new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Set the URI host'), ]) ->setHelp(<<<'EOF' -The %command.name% shows which routes match a given request and which don't and for what reason: + The %command.name% shows which routes match a given request and which don't and for what reason: - php %command.full_name% /foo + php %command.full_name% /foo -or + or - php %command.full_name% /foo --method POST --scheme https --host symfony.com --verbose + php %command.full_name% /foo --method POST --scheme https --host symfony.com --verbose -EOF + EOF ) ; } diff --git a/Command/SecretsDecryptToLocalCommand.php b/Command/SecretsDecryptToLocalCommand.php index 4e392b677..3dee29450 100644 --- a/Command/SecretsDecryptToLocalCommand.php +++ b/Command/SecretsDecryptToLocalCommand.php @@ -40,14 +40,14 @@ protected function configure(): void $this ->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. + The %command.name% command decrypts all secrets and copies them in the local vault. - %command.full_name% + %command.full_name% -When the --force option is provided, secrets that already exist in the local vault are overridden. + When the --force option is provided, secrets that already exist in the local vault are overridden. - %command.full_name% --force -EOF + %command.full_name% --force + EOF ) ; } diff --git a/Command/SecretsEncryptFromLocalCommand.php b/Command/SecretsEncryptFromLocalCommand.php index 11591d730..a99ebf0ac 100644 --- a/Command/SecretsEncryptFromLocalCommand.php +++ b/Command/SecretsEncryptFromLocalCommand.php @@ -38,10 +38,10 @@ protected function configure(): void { $this ->setHelp(<<<'EOF' -The %command.name% command encrypts all locally overridden secrets to the vault. + The %command.name% command encrypts all locally overridden secrets to the vault. - %command.full_name% -EOF + %command.full_name% + EOF ) ; } diff --git a/Command/SecretsGenerateKeysCommand.php b/Command/SecretsGenerateKeysCommand.php index f721c786e..e0d5d9c52 100644 --- a/Command/SecretsGenerateKeysCommand.php +++ b/Command/SecretsGenerateKeysCommand.php @@ -43,16 +43,16 @@ protected function configure(): void ->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' -The %command.name% command generates a new encryption key. + The %command.name% command generates a new encryption key. - %command.full_name% + %command.full_name% -If encryption keys already exist, the command must be called with -the --rotate option in order to override those keys and re-encrypt -existing secrets. + If encryption keys already exist, the command must be called with + the --rotate option in order to override those keys and re-encrypt + existing secrets. - %command.full_name% --rotate -EOF + %command.full_name% --rotate + EOF ) ; } diff --git a/Command/SecretsListCommand.php b/Command/SecretsListCommand.php index 920b3b1fc..9057f58d1 100644 --- a/Command/SecretsListCommand.php +++ b/Command/SecretsListCommand.php @@ -43,14 +43,14 @@ protected function configure(): void $this ->addOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names') ->setHelp(<<<'EOF' -The %command.name% command list all stored secrets. + The %command.name% command list all stored secrets. - %command.full_name% + %command.full_name% -When the option --reveal is provided, the decrypted secrets are also displayed. + When the option --reveal is provided, the decrypted secrets are also displayed. - %command.full_name% --reveal -EOF + %command.full_name% --reveal + EOF ) ; } diff --git a/Command/SecretsRemoveCommand.php b/Command/SecretsRemoveCommand.php index 59bbe8211..2c3bbb18e 100644 --- a/Command/SecretsRemoveCommand.php +++ b/Command/SecretsRemoveCommand.php @@ -45,10 +45,10 @@ protected function configure(): void ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret') ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') ->setHelp(<<<'EOF' -The %command.name% command removes a secret from the vault. + The %command.name% command removes a secret from the vault. - %command.full_name% -EOF + %command.full_name% + EOF ) ; } diff --git a/Command/SecretsRevealCommand.php b/Command/SecretsRevealCommand.php index c2110ee76..8a678e14d 100644 --- a/Command/SecretsRevealCommand.php +++ b/Command/SecretsRevealCommand.php @@ -38,10 +38,10 @@ protected function configure(): void $this ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret to reveal', null, fn () => array_keys($this->vault->list())) ->setHelp(<<<'EOF' -The %command.name% command reveals a stored secret. + The %command.name% command reveals a stored secret. - %command.full_name% -EOF + %command.full_name% + EOF ) ; } diff --git a/Command/SecretsSetCommand.php b/Command/SecretsSetCommand.php index f7e8eeaa6..c9eabb25a 100644 --- a/Command/SecretsSetCommand.php +++ b/Command/SecretsSetCommand.php @@ -48,24 +48,24 @@ protected function configure(): void ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') ->addOption('random', 'r', InputOption::VALUE_OPTIONAL, 'Generate a random value.', false) ->setHelp(<<<'EOF' -The %command.name% command stores a secret in the vault. + The %command.name% command stores a secret in the vault. - %command.full_name% + %command.full_name% -To reference secrets in services.yaml or any other config -files, use "%env()%". + To reference secrets in services.yaml or any other config + files, use "%env()%". -By default, the secret value should be entered interactively. -Alternatively, provide a file where to read the secret from: + By default, the secret value should be entered interactively. + Alternatively, provide a file where to read the secret from: - php %command.full_name% filename + php %command.full_name% filename -Use "-" as a file name to read from STDIN: + Use "-" as a file name to read from STDIN: - cat filename | php %command.full_name% - + cat filename | php %command.full_name% - -Use --local to override secrets for local needs. -EOF + Use --local to override secrets for local needs. + EOF ) ; } diff --git a/Command/TranslationDebugCommand.php b/Command/TranslationDebugCommand.php index a320130d5..b186646a3 100644 --- a/Command/TranslationDebugCommand.php +++ b/Command/TranslationDebugCommand.php @@ -75,35 +75,35 @@ protected function configure(): void new InputOption('all', null, InputOption::VALUE_NONE, 'Load messages from all registered bundles'), ]) ->setHelp(<<<'EOF' -The %command.name% command helps finding unused or missing translation -messages and comparing them with the fallback ones by inspecting the -templates and translation files of a given bundle or the default translations directory. + The %command.name% command helps finding unused or missing translation + messages and comparing them with the fallback ones by inspecting the + templates and translation files of a given bundle or the default translations directory. -You can display information about bundle translations in a specific locale: + You can display information about bundle translations in a specific locale: - php %command.full_name% en AcmeDemoBundle + php %command.full_name% en AcmeDemoBundle -You can also specify a translation domain for the search: + You can also specify a translation domain for the search: - php %command.full_name% --domain=messages en AcmeDemoBundle + php %command.full_name% --domain=messages en AcmeDemoBundle -You can only display missing messages: + You can only display missing messages: - php %command.full_name% --only-missing en AcmeDemoBundle + php %command.full_name% --only-missing en AcmeDemoBundle -You can only display unused messages: + You can only display unused messages: - php %command.full_name% --only-unused en AcmeDemoBundle + php %command.full_name% --only-unused en AcmeDemoBundle -You can display information about application translations in a specific locale: + You can display information about application translations in a specific locale: - php %command.full_name% en + php %command.full_name% en -You can display information about translations in all registered bundles in a specific locale: + You can display information about translations in all registered bundles in a specific locale: - php %command.full_name% --all en + php %command.full_name% --all en -EOF + EOF ) ; } diff --git a/Command/TranslationExtractCommand.php b/Command/TranslationExtractCommand.php index c8e61b61a..32f19fbe4 100644 --- a/Command/TranslationExtractCommand.php +++ b/Command/TranslationExtractCommand.php @@ -83,34 +83,34 @@ protected function configure(): void new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), ]) ->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 -the new ones into the translation files. + The %command.name% command extracts translation strings from templates + of a given bundle or the default translations directory. It can display them or merge + the new ones into the translation files. -When new translation strings are found it can automatically add a prefix to the translation -message. However, if the --no-fill option is used, the --prefix -option has no effect, since the translation values are left empty. + When new translation strings are found it can automatically add a prefix to the translation + message. However, if the --no-fill option is used, the --prefix + option has no effect, since the translation values are left empty. -Example running against a Bundle (AcmeBundle) + Example running against a Bundle (AcmeBundle) - php %command.full_name% --dump-messages en AcmeBundle - php %command.full_name% --force --prefix="new_" fr AcmeBundle + php %command.full_name% --dump-messages en AcmeBundle + php %command.full_name% --force --prefix="new_" fr AcmeBundle -Example running against default messages directory + Example running against default messages directory - php %command.full_name% --dump-messages en - php %command.full_name% --force --prefix="new_" fr + php %command.full_name% --dump-messages en + php %command.full_name% --force --prefix="new_" fr -You can sort the output with the --sort flag: + You can sort the output with the --sort flag: - php %command.full_name% --dump-messages --sort=asc en AcmeBundle - php %command.full_name% --force --sort=desc fr + php %command.full_name% --dump-messages --sort=asc en AcmeBundle + php %command.full_name% --force --sort=desc fr -You can dump a tree-like structure using the yaml format with --as-tree flag: + 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 --as-tree=3 en AcmeBundle -EOF + EOF ) ; } diff --git a/Command/WorkflowDumpCommand.php b/Command/WorkflowDumpCommand.php index 1fcb35424..7cf43b524 100644 --- a/Command/WorkflowDumpCommand.php +++ b/Command/WorkflowDumpCommand.php @@ -12,120 +12,14 @@ 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\DependencyInjection\ServiceLocator; -use Symfony\Component\Workflow\Debug\TraceableWorkflow; -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; -use Symfony\Component\Workflow\StateMachine; +use Symfony\Component\Workflow\Command\WorkflowDumpCommand as BaseWorkflowDumpCommand; + +trigger_deprecation('symfony/framework-bundle', '7.4', 'The "%s" class is deprecated, use "%s" instead.', WorkflowDumpCommand::class, BaseWorkflowDumpCommand::class); /** - * @author Grégoire Pineau - * - * @final + * @deprecated since Symfony 7.4, use {@see BaseWorkflowDumpCommand} instead. */ #[AsCommand(name: 'workflow:dump', description: 'Dump a workflow')] -class WorkflowDumpCommand extends Command +class WorkflowDumpCommand extends BaseWorkflowDumpCommand { - private const DUMP_FORMAT_OPTIONS = [ - 'puml', - 'mermaid', - 'dot', - ]; - - public function __construct( - private ServiceLocator $workflows, - ) { - parent::__construct(); - } - - protected function configure(): void - { - $this - ->setDefinition([ - 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('with-metadata', null, InputOption::VALUE_NONE, 'Include the workflow\'s metadata in the dumped graph', null), - new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format ['.implode('|', self::DUMP_FORMAT_OPTIONS).']', 'dot'), - ]) - ->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 - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $workflowName = $input->getArgument('name'); - - if (!$this->workflows->has($workflowName)) { - throw new InvalidArgumentException(\sprintf('The workflow named "%s" cannot be found.', $workflowName)); - } - $workflow = $this->workflows->get($workflowName); - if ($workflow instanceof TraceableWorkflow) { - $workflow = $workflow->getInner(); - } - $type = $workflow instanceof StateMachine ? 'state_machine' : 'workflow'; - $definition = $workflow->getDefinition(); - - 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(); - - foreach ($input->getArgument('marking') as $place) { - $marking->mark($place); - } - - $options = [ - 'name' => $workflowName, - 'with-metadata' => $input->getOption('with-metadata'), - 'nofooter' => true, - 'label' => $input->getOption('label'), - ]; - $output->writeln($dumper->dump($definition, $marking, $options)); - - return 0; - } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('name')) { - $suggestions->suggestValues(array_keys($this->workflows->getProvidedServices())); - } - - if ($input->mustSuggestOptionValuesFor('dump-format')) { - $suggestions->suggestValues(self::DUMP_FORMAT_OPTIONS); - } - } } diff --git a/Command/XliffLintCommand.php b/Command/XliffLintCommand.php index 5b094f165..9bbe39db1 100644 --- a/Command/XliffLintCommand.php +++ b/Command/XliffLintCommand.php @@ -47,11 +47,11 @@ protected function configure(): void $this->setHelp($this->getHelp().<<<'EOF' -Or find all files in a bundle: + Or find all files in a bundle: - php %command.full_name% @AcmeDemoBundle + php %command.full_name% @AcmeDemoBundle -EOF + EOF ); } } diff --git a/Command/YamlLintCommand.php b/Command/YamlLintCommand.php index 141390812..5948add7c 100644 --- a/Command/YamlLintCommand.php +++ b/Command/YamlLintCommand.php @@ -46,11 +46,11 @@ protected function configure(): void $this->setHelp($this->getHelp().<<<'EOF' -Or find all files in a bundle: + Or find all files in a bundle: - php %command.full_name% @AcmeDemoBundle + php %command.full_name% @AcmeDemoBundle -EOF + EOF ); } } diff --git a/Console/Application.php b/Console/Application.php index 274e7b06d..8eb3808a5 100644 --- a/Console/Application.php +++ b/Console/Application.php @@ -159,11 +159,29 @@ public function getLongVersion(): string return parent::getLongVersion().\sprintf(' (env: %s, debug: %s)', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); } + /** + * @deprecated since Symfony 7.4, use Application::addCommand() instead + */ public function add(Command $command): ?Command + { + trigger_deprecation('symfony/framework-bundle', '7.4', 'The "%s()" method is deprecated and will be removed in Symfony 8.0, use "%s::addCommand()" instead.', __METHOD__, self::class); + + return $this->addCommand($command); + } + + public function addCommand(callable|Command $command): ?Command { $this->registerCommands(); - return parent::add($command); + if (!method_exists(BaseApplication::class, 'addCommand')) { + if (!$command instanceof Command) { + throw new \LogicException('Using callables as commands requires symfony/console 7.4 or higher.'); + } + + return parent::add($command); + } + + return parent::addCommand($command); } protected function registerCommands(): void @@ -197,7 +215,7 @@ protected function registerCommands(): void foreach ($container->getParameter('console.command.ids') as $id) { if (!isset($lazyCommandIds[$id])) { try { - $this->add($container->get($id)); + $this->addCommand($container->get($id)); } catch (\Throwable $e) { $this->registrationErrors[] = $e; } diff --git a/Console/Descriptor/JsonDescriptor.php b/Console/Descriptor/JsonDescriptor.php index c7705a1a0..cfae075ed 100644 --- a/Console/Descriptor/JsonDescriptor.php +++ b/Console/Descriptor/JsonDescriptor.php @@ -248,7 +248,7 @@ private function getContainerDefinitionData(Definition $definition, bool $omitTa { $data = [ 'class' => (string) $definition->getClass(), - 'public' => $definition->isPublic() && !$definition->isPrivate(), + 'public' => $definition->isPublic(), 'synthetic' => $definition->isSynthetic(), 'lazy' => $definition->isLazy(), 'shared' => $definition->isShared(), @@ -313,7 +313,7 @@ private function getContainerAliasData(Alias $alias): array { return [ 'service' => (string) $alias, - 'public' => $alias->isPublic() && !$alias->isPrivate(), + 'public' => $alias->isPublic(), ]; } diff --git a/Console/Descriptor/MarkdownDescriptor.php b/Console/Descriptor/MarkdownDescriptor.php index d057c598d..faa92e918 100644 --- a/Console/Descriptor/MarkdownDescriptor.php +++ b/Console/Descriptor/MarkdownDescriptor.php @@ -214,7 +214,7 @@ protected function describeContainerDefinition(Definition $definition, array $op } $output .= '- Class: `'.$definition->getClass().'`' - ."\n".'- Public: '.($definition->isPublic() && !$definition->isPrivate() ? 'yes' : 'no') + ."\n".'- Public: '.($definition->isPublic() ? 'yes' : 'no') ."\n".'- Synthetic: '.($definition->isSynthetic() ? 'yes' : 'no') ."\n".'- Lazy: '.($definition->isLazy() ? 'yes' : 'no') ."\n".'- Shared: '.($definition->isShared() ? 'yes' : 'no') @@ -276,7 +276,7 @@ protected function describeContainerDefinition(Definition $definition, array $op protected function describeContainerAlias(Alias $alias, array $options = [], ?ContainerBuilder $container = null): void { $output = '- Service: `'.$alias.'`' - ."\n".'- Public: '.($alias->isPublic() && !$alias->isPrivate() ? 'yes' : 'no'); + ."\n".'- Public: '.($alias->isPublic() ? 'yes' : 'no'); if (!isset($options['id'])) { $this->write($output); diff --git a/Console/Descriptor/TextDescriptor.php b/Console/Descriptor/TextDescriptor.php index 12b345411..69e4b395c 100644 --- a/Console/Descriptor/TextDescriptor.php +++ b/Console/Descriptor/TextDescriptor.php @@ -38,6 +38,18 @@ */ class TextDescriptor extends Descriptor { + private const VERB_COLORS = [ + 'ANY' => 'default', + 'GET' => 'blue', + 'QUERY' => 'blue', + 'HEAD' => 'magenta', + 'OPTIONS' => 'blue', + 'POST' => 'green', + 'PUT' => 'yellow', + 'PATCH' => 'yellow', + 'DELETE' => 'red', + ]; + public function __construct( private ?FileLinkFormatter $fileLinkFormatter = null, ) { @@ -45,40 +57,64 @@ public function __construct( protected function describeRouteCollection(RouteCollection $routes, array $options = []): void { - $showControllers = isset($options['show_controllers']) && $options['show_controllers']; - - $tableHeaders = ['Name', 'Method', 'Scheme', 'Host', 'Path']; - if ($showControllers) { - $tableHeaders[] = 'Controller'; - } - - if ($showAliases = $options['show_aliases'] ?? false) { - $tableHeaders[] = 'Aliases'; - } + $showAliases = $options['show_aliases'] ?? false; + $showControllers = $options['show_controllers'] ?? false; $tableRows = []; + $shouldShowScheme = false; + $shouldShowHost = false; foreach ($routes->all() as $name => $route) { $controller = $route->getDefault('_controller'); + $scheme = $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'; + $shouldShowScheme = $shouldShowScheme || 'ANY' !== $scheme; + + $host = '' !== $route->getHost() ? $route->getHost() : 'ANY'; + $shouldShowHost = $shouldShowHost || 'ANY' !== $host; + $row = [ - $name, - $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY', - $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY', - '' !== $route->getHost() ? $route->getHost() : 'ANY', - $this->formatControllerLink($controller, $route->getPath(), $options['container'] ?? null), + 'Name' => $name, + 'Methods' => $this->formatMethods($route->getMethods()), + 'Scheme' => $scheme, + 'Host' => $host, + 'Path' => $route->getPath(), ]; if ($showControllers) { - $row[] = $controller ? $this->formatControllerLink($controller, $this->formatCallable($controller), $options['container'] ?? null) : ''; + $row['Controller'] = $controller ? $this->formatControllerLink($controller, $this->formatCallable($controller), $options['container'] ?? null) : ''; } if ($showAliases) { - $row[] = implode('|', ($reverseAliases ??= $this->getReverseAliases($routes))[$name] ?? []); + $row['Aliases'] = implode('|', $this->getReverseAliases($routes)[$name] ?? []); } $tableRows[] = $row; } + $tableHeaders = ['Name', 'Method']; + + if ($shouldShowScheme) { + $tableHeaders[] = 'Scheme'; + } else { + array_walk($tableRows, function (&$row) { unset($row['Scheme']); }); + } + + if ($shouldShowHost) { + $tableHeaders[] = 'Host'; + } else { + array_walk($tableRows, function (&$row) { unset($row['Host']); }); + } + + $tableHeaders[] = 'Path'; + + if ($showControllers) { + $tableHeaders[] = 'Controller'; + } + + if ($showAliases) { + $tableHeaders[] = 'Aliases'; + } + if (isset($options['output'])) { $options['output']->table($tableHeaders, $tableRows); } else { @@ -103,7 +139,7 @@ protected function describeRoute(Route $route, array $options = []): void ['Host', '' !== $route->getHost() ? $route->getHost() : 'ANY'], ['Host Regex', '' !== $route->getHost() ? $route->compile()->getHostRegex() : ''], ['Scheme', $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'], - ['Method', $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'], + ['Method', $this->formatMethods($route->getMethods())], ['Requirements', $route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM'], ['Class', $route::class], ['Defaults', $this->formatRouterConfig($defaults)], @@ -324,7 +360,7 @@ protected function describeContainerDefinition(Definition $definition, array $op $tableRows[] = ['Calls', implode(', ', $callInformation)]; } - $tableRows[] = ['Public', $definition->isPublic() && !$definition->isPrivate() ? 'yes' : 'no']; + $tableRows[] = ['Public', $definition->isPublic() ? 'yes' : 'no']; $tableRows[] = ['Synthetic', $definition->isSynthetic() ? 'yes' : 'no']; $tableRows[] = ['Lazy', $definition->isLazy() ? 'yes' : 'no']; $tableRows[] = ['Shared', $definition->isShared() ? 'yes' : 'no']; @@ -419,7 +455,7 @@ protected function describeContainerDeprecations(ContainerBuilder $container, ar protected function describeContainerAlias(Alias $alias, array $options = [], ?ContainerBuilder $container = null): void { - if ($alias->isPublic() && !$alias->isPrivate()) { + if ($alias->isPublic()) { $options['output']->comment(\sprintf('This service is a public alias for the service %s', (string) $alias)); } else { $options['output']->comment(\sprintf('This service is a private alias for the service %s', (string) $alias)); @@ -576,6 +612,24 @@ private function formatRouterConfig(array $config): string return trim($configAsString); } + /** + * @param array $methods + */ + private function formatMethods(array $methods): string + { + if ([] === $methods) { + $methods = ['ANY']; + } + + return implode('|', array_map( + fn (string $method): string => \sprintf('%s', self::VERB_COLORS[$method] ?? 'default', $method), + $methods + )); + } + + /** + * @param (callable():ContainerBuilder)|null $getContainer + */ private function formatControllerLink(mixed $controller, string $anchorText, ?callable $getContainer = null): string { if (null === $this->fileLinkFormatter) { diff --git a/Console/Descriptor/XmlDescriptor.php b/Console/Descriptor/XmlDescriptor.php index 8daa61d2a..08ef443f1 100644 --- a/Console/Descriptor/XmlDescriptor.php +++ b/Console/Descriptor/XmlDescriptor.php @@ -288,6 +288,9 @@ private function getContainerServiceDocument(object $service, string $id, ?Conta return $dom; } + /** + * @param (callable(string):bool)|null $filter + */ private function getContainerServicesDocument(ContainerBuilder $container, ?string $tag = null, bool $showHidden = false, ?callable $filter = null): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); @@ -351,7 +354,7 @@ private function getContainerDefinitionDocument(Definition $definition, ?string } } - $serviceXML->setAttribute('public', $definition->isPublic() && !$definition->isPrivate() ? 'true' : 'false'); + $serviceXML->setAttribute('public', $definition->isPublic() ? 'true' : 'false'); $serviceXML->setAttribute('synthetic', $definition->isSynthetic() ? 'true' : 'false'); $serviceXML->setAttribute('lazy', $definition->isLazy() ? 'true' : 'false'); $serviceXML->setAttribute('shared', $definition->isShared() ? 'true' : 'false'); @@ -474,7 +477,7 @@ private function getContainerAliasDocument(Alias $alias, ?string $id = null): \D } $aliasXML->setAttribute('service', (string) $alias); - $aliasXML->setAttribute('public', $alias->isPublic() && !$alias->isPrivate() ? 'true' : 'false'); + $aliasXML->setAttribute('public', $alias->isPublic() ? 'true' : 'false'); return $dom; } diff --git a/Controller/AbstractController.php b/Controller/AbstractController.php index de7395d5a..c44028f8c 100644 --- a/Controller/AbstractController.php +++ b/Controller/AbstractController.php @@ -67,18 +67,6 @@ public function setContainer(ContainerInterface $container): ?ContainerInterface return $previous; } - /** - * Gets a container parameter by its 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)); - } - - return $this->container->get('parameter_bag')->get($name); - } - public static function getSubscribedServices(): array { return [ @@ -96,6 +84,18 @@ public static function getSubscribedServices(): array ]; } + /** + * Gets a container parameter by its 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)); + } + + return $this->container->get('parameter_bag')->get($name); + } + /** * Generates a URL from the given parameters. * diff --git a/Controller/ControllerHelper.php b/Controller/ControllerHelper.php new file mode 100644 index 000000000..4fc56b6a9 --- /dev/null +++ b/Controller/ControllerHelper.php @@ -0,0 +1,473 @@ + + * + * 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 Psr\Container\ContainerInterface; +use Psr\Link\EvolvableLinkInterface; +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\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\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +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\AccessDecision; +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\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; +use Symfony\Component\WebLink\HttpHeaderSerializer; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Twig\Environment; + +/** + * Provides the helpers from AbstractControler as a standalone service. + * + * Best used together with #[AutowireMethodOf] to remove any coupling. + */ +class ControllerHelper implements ServiceSubscriberInterface +{ + public function __construct( + private ContainerInterface $container, + ) { + } + + public static function getSubscribedServices(): array + { + return [ + 'router' => '?'.RouterInterface::class, + 'request_stack' => '?'.RequestStack::class, + 'http_kernel' => '?'.HttpKernelInterface::class, + 'serializer' => '?'.SerializerInterface::class, + 'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class, + 'twig' => '?'.Environment::class, + 'form.factory' => '?'.FormFactoryInterface::class, + 'security.token_storage' => '?'.TokenStorageInterface::class, + 'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class, + 'parameter_bag' => '?'.ContainerBagInterface::class, + 'web_link.http_header_serializer' => '?'.HttpHeaderSerializer::class, + ]; + } + + /** + * Gets a container parameter by its name. + */ + public 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)); + } + + return $this->container->get('parameter_bag')->get($name); + } + + /** + * Generates a URL from the given parameters. + * + * @see UrlGeneratorInterface + */ + public 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 "App\Controller\PostController::index" or "App\Controller\PostController" if it is invokable) + */ + public 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. + * + * @param int $status The HTTP status code (302 "Found" by default) + */ + public function redirect(string $url, int $status = 302): RedirectResponse + { + return new RedirectResponse($url, $status); + } + + /** + * Returns a RedirectResponse to the given route with the given parameters. + * + * @param int $status The HTTP status code (302 "Found" by default) + */ + public 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. + * + * @param int $status The HTTP status code (200 "OK" by default) + */ + public 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. + */ + public function file(\SplFileInfo|string $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 + */ + public function addFlash(string $type, mixed $message): void + { + try { + $session = $this->container->get('request_stack')->getSession(); + } 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); + } + + if (!$session instanceof FlashBagAwareSessionInterface) { + throw new \LogicException(\sprintf('You cannot use the addFlash method because class "%s" doesn\'t implement "%s".', get_debug_type($session), FlashBagAwareSessionInterface::class)); + } + + $session->getFlashBag()->add($type, $message); + } + + /** + * Checks if the attribute is granted against the current authentication token and optionally supplied subject. + * + * @throws \LogicException + */ + public 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); + } + + /** + * Checks if the attribute is granted against the current authentication token and optionally supplied subject. + */ + public function getAccessDecision(mixed $attribute, mixed $subject = null): AccessDecision + { + 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".'); + } + + $accessDecision = new AccessDecision(); + $accessDecision->isGranted = $this->container->get('security.authorization_checker')->isGranted($attribute, $subject, $accessDecision); + + return $accessDecision; + } + + /** + * Throws an exception unless the attribute is granted against the current authentication token and optionally + * supplied subject. + * + * @throws AccessDeniedException + */ + public function denyAccessUnlessGranted(mixed $attribute, mixed $subject = null, string $message = 'Access Denied.'): void + { + if (class_exists(AccessDecision::class)) { + $accessDecision = $this->getAccessDecision($attribute, $subject); + $isGranted = $accessDecision->isGranted; + } else { + $accessDecision = null; + $isGranted = $this->isGranted($attribute, $subject); + } + + if (!$isGranted) { + $e = $this->createAccessDeniedException(3 > \func_num_args() && $accessDecision ? $accessDecision->getMessage() : $message); + $e->setAttributes([$attribute]); + $e->setSubject($subject); + + if ($accessDecision) { + $e->setAccessDecision($accessDecision); + } + + throw $e; + } + } + + /** + * Returns a rendered view. + * + * Forms found in parameters are auto-cast to form views. + */ + public function renderView(string $view, array $parameters = []): string + { + return $this->doRenderView($view, null, $parameters, __FUNCTION__); + } + + /** + * Returns a rendered block from a view. + * + * Forms found in parameters are auto-cast to form views. + */ + public function renderBlockView(string $view, string $block, array $parameters = []): string + { + return $this->doRenderView($view, $block, $parameters, __FUNCTION__); + } + + /** + * Renders a view. + * + * If an invalid form is found in the list of parameters, a 422 status code is returned. + * Forms found in parameters are auto-cast to form views. + */ + public function render(string $view, array $parameters = [], ?Response $response = null): Response + { + return $this->doRender($view, null, $parameters, $response, __FUNCTION__); + } + + /** + * Renders a block in a view. + * + * If an invalid form is found in the list of parameters, a 422 status code is returned. + * Forms found in parameters are auto-cast to form views. + */ + public function renderBlock(string $view, string $block, array $parameters = [], ?Response $response = null): Response + { + return $this->doRender($view, $block, $parameters, $response, __FUNCTION__); + } + + /** + * Streams a view. + */ + public 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!'); + */ + public 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 + */ + public 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. + */ + public 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. + */ + public 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() + */ + public 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 + */ + public function isCsrfTokenValid(string $id, #[\SensitiveParameter] ?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 + */ + public 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)); + } + + /** + * @param LinkInterface[] $links + */ + public function sendEarlyHints(iterable $links = [], ?Response $response = null): Response + { + if (!$this->container->has('web_link.http_header_serializer')) { + throw new \LogicException('You cannot use the "sendEarlyHints" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); + } + + $response ??= new Response(); + + $populatedLinks = []; + foreach ($links as $link) { + if ($link instanceof EvolvableLinkInterface && !$link->getRels()) { + $link = $link->withRel('preload'); + } + + $populatedLinks[] = $link; + } + + $response->headers->set('Link', $this->container->get('web_link.http_header_serializer')->serialize($populatedLinks), false); + $response->sendHeaders(103); + + return $response; + } + + private function doRenderView(string $view, ?string $block, array $parameters, string $method): string + { + if (!$this->container->has('twig')) { + throw new \LogicException(\sprintf('You cannot use the "%s" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".', $method)); + } + + foreach ($parameters as $k => $v) { + if ($v instanceof FormInterface) { + $parameters[$k] = $v->createView(); + } + } + + if (null !== $block) { + return $this->container->get('twig')->load($view)->renderBlock($block, $parameters); + } + + return $this->container->get('twig')->render($view, $parameters); + } + + private function doRender(string $view, ?string $block, array $parameters, ?Response $response, string $method): Response + { + $content = $this->doRenderView($view, $block, $parameters, $method); + $response ??= new Response(); + + if (200 === $response->getStatusCode()) { + foreach ($parameters as $v) { + if ($v instanceof FormInterface && $v->isSubmitted() && !$v->isValid()) { + $response->setStatusCode(422); + break; + } + } + } + + $response->setContent($content); + + return $response; + } +} diff --git a/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php index e4023e623..456305bc9 100644 --- a/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php +++ b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php @@ -13,8 +13,11 @@ use Symfony\Component\Config\ConfigCache; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ResolveEnvPlaceholdersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\XmlDumper; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; +use Symfony\Component\Filesystem\Filesystem; /** * Dumps the ContainerBuilder to a cache file so that it can be used by @@ -31,9 +34,52 @@ public function process(ContainerBuilder $container): void return; } - $cache = new ConfigCache($container->getParameter('debug.container.dump'), true); - if (!$cache->isFresh()) { - $cache->write((new XmlDumper($container))->dump(), $container->getResources()); + $file = $container->getParameter('debug.container.dump'); + $cache = new ConfigCache($file, true); + if ($cache->isFresh()) { + return; + } + $cache->write((new XmlDumper($container))->dump(), $container->getResources()); + + if (!str_ends_with($file, '.xml')) { + return; + } + + $file = substr_replace($file, '.ser', -4); + + try { + $dump = new ContainerBuilder(clone $container->getParameterBag()); + $dump->setDefinitions(unserialize(serialize($container->getDefinitions()))); + $dump->setAliases($container->getAliases()); + + if (($bag = $container->getParameterBag()) instanceof EnvPlaceholderParameterBag) { + (new ResolveEnvPlaceholdersPass(null))->process($dump); + $dump->__construct(new EnvPlaceholderParameterBag($container->resolveEnvPlaceholders($this->escapeParameters($bag->all())))); + } + + $fs = new Filesystem(); + $fs->dumpFile($file, serialize($dump)); + $fs->chmod($file, 0o666, umask()); + } catch (\Throwable $e) { + $container->getCompiler()->log($this, $e->getMessage()); + // ignore serialization and file-system errors + if (file_exists($file)) { + @unlink($file); + } } } + + private function escapeParameters(array $parameters): array + { + $params = []; + foreach ($parameters as $k => $v) { + $params[$k] = match (true) { + \is_array($v) => $this->escapeParameters($v), + \is_string($v) => str_replace('%', '%%', $v), + default => $v, + }; + } + + return $params; + } } diff --git a/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php b/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php new file mode 100644 index 000000000..cc220eb88 --- /dev/null +++ b/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php @@ -0,0 +1,192 @@ + + * + * 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\Config\Definition\ArrayShapeGenerator; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\Configurator\AppReference; +use Symfony\Component\Routing\Loader\Configurator\RoutesReference; + +/** + * @internal + */ +class PhpConfigReferenceDumpPass implements CompilerPassInterface +{ + private const REFERENCE_TEMPLATE = <<<'PHP' + + * } + */ + PHPDOC; + + private const WHEN_ENV_ROUTES_TEMPLATE = <<<'PHPDOC' + + * "when@{ENV}"?: array, + PHPDOC; + + public function __construct( + private string $referenceFile, + private array $bundlesDefinition, + ) { + } + + public function process(ContainerBuilder $container): void + { + $knownEnvs = $container->hasParameter('.container.known_envs') ? $container->getParameter('.container.known_envs') : [$container->getParameter('kernel.environment')]; + $knownEnvs = array_unique($knownEnvs); + sort($knownEnvs); + $extensionsPerEnv = []; + $appTypes = ''; + + $anyEnvExtensions = []; + foreach ($this->bundlesDefinition as $bundle => $envs) { + if (!$extension = (new $bundle())->getContainerExtension()) { + continue; + } + if (!$configuration = $this->getConfiguration($extension, $container)) { + continue; + } + $anyEnvExtensions[$bundle] = $extension; + $type = $this->camelCase($extension->getAlias()).'Config'; + $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree())); + + foreach ($knownEnvs as $env) { + if ($envs[$env] ?? $envs['all'] ?? false) { + $extensionsPerEnv[$env][] = $extension; + } else { + unset($anyEnvExtensions[$bundle]); + } + } + } + krsort($extensionsPerEnv); + + $r = new \ReflectionClass(AppReference::class); + + if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n */")) { + throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', AppReference::class)); + } + $appTypes = substr_replace($phpdoc, $appTypes, $i, 0); + + if (false === $i = strpos($phpdoc = $r->getMethod('config')->getDocComment(), "\n * ...getShapeForExtensions($anyEnvExtensions, $container), $i, 0); + $i += \strlen($appParam) - \strlen($phpdoc); + + foreach ($extensionsPerEnv as $env => $extensions) { + $appParam = substr_replace($appParam, strtr(self::WHEN_ENV_APP_TEMPLATE, [ + '{ENV}' => $env, + '{SHAPE}' => $this->getShapeForExtensions($extensions, $container, ' '), + ]), $i, 0); + } + + $r = new \ReflectionClass(RoutesReference::class); + + if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n * @psalm-type RoutesConfig = ")) { + throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', RoutesReference::class)); + } + $routesTypes = ''; + foreach ($knownEnvs as $env) { + $routesTypes .= strtr(self::WHEN_ENV_ROUTES_TEMPLATE, ['{ENV}' => $env]); + } + if ('' !== $routesTypes) { + $routesTypes = strtr(self::ROUTES_TYPES_TEMPLATE, ['{SHAPE}' => $routesTypes]); + $routesTypes = substr_replace($phpdoc, $routesTypes, $i); + } + + $configReference = strtr(self::REFERENCE_TEMPLATE, [ + '{APP_TYPES}' => $appTypes, + '{APP_PARAM}' => $appParam, + '{ROUTES_TYPES}' => $routesTypes, + '{ROUTES_PARAM}' => $r->getMethod('config')->getDocComment(), + ]); + + $dir = \dirname($this->referenceFile); + if (is_dir($dir) && is_writable($dir) && (!is_file($this->referenceFile) || file_get_contents($this->referenceFile) !== $configReference)) { + file_put_contents($this->referenceFile, $configReference); + } + } + + private function camelCase(string $input): string + { + $output = ucfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + + return preg_replace('#\W#', '', $output); + } + + private function getConfiguration(ExtensionInterface $extension, ContainerBuilder $container): ?ConfigurationInterface + { + return match (true) { + $extension instanceof ConfigurationInterface => $extension, + $extension instanceof ConfigurationExtensionInterface => $extension->getConfiguration([], $container), + default => null, + }; + } + + private function getShapeForExtensions(array $extensions, ContainerBuilder $container, string $indent = ''): string + { + $shape = ''; + foreach ($extensions as $extension) { + if ($this->getConfiguration($extension, $container)) { + $type = $this->camelCase($extension->getAlias()).'Config'; + $shape .= \sprintf("\n * %s%s?: %s,", $indent, $extension->getAlias(), $type); + } + } + + return $shape; + } +} diff --git a/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php b/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php index 3b3dfcc06..084c6066b 100644 --- a/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php +++ b/DependencyInjection/Compiler/TestServiceContainerWeakRefPass.php @@ -31,7 +31,7 @@ public function process(ContainerBuilder $container): void $definitions = $container->getDefinitions(); foreach ($definitions as $id => $definition) { - if ($id && '.' !== $id[0] && (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) && !$definition->hasErrors() && !$definition->isAbstract()) { + if ($id && '.' !== $id[0] && ($definition->isPrivate() || $definition->hasTag('container.private')) && !$definition->hasErrors() && !$definition->isAbstract()) { $privateServices[$id] = new Reference($id, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE); } } @@ -39,7 +39,7 @@ public function process(ContainerBuilder $container): void $aliases = $container->getAliases(); foreach ($aliases as $id => $alias) { - if ($id && '.' !== $id[0] && (!$alias->isPublic() || $alias->isPrivate())) { + if ($id && '.' !== $id[0] && $alias->isPrivate()) { while (isset($aliases[$target = (string) $alias])) { $alias = $aliases[$target]; } diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 53361e312..36e3ee1ae 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -78,6 +78,7 @@ class UnusedTagsPass implements CompilerPassInterface 'proxy', 'remote_event.consumer', 'routing.condition_service', + 'routing.controller', 'routing.expression_language_function', 'routing.expression_language_provider', 'routing.loader', @@ -101,6 +102,7 @@ class UnusedTagsPass implements CompilerPassInterface 'twig.extension', 'twig.loader', 'twig.runtime', + 'validator.attribute_metadata', 'validator.auto_mapper', 'validator.constraint_validator', 'validator.group_provider', diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index d65895c41..662ad38b4 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -26,6 +26,7 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\LogicException; +use Symfony\Component\Finder\Glob; use Symfony\Component\Form\Form; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; use Symfony\Component\HttpClient\HttpClient; @@ -78,21 +79,30 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/framework.html', 'symfony/framework-bundle') ->beforeNormalization() - ->ifTrue(fn ($v) => !isset($v['assets']) && isset($v['templating']) && class_exists(Package::class)) - ->then(function ($v) { - $v['assets'] = []; + ->ifArray() + ->then(static function ($v) { + if (isset($v['templating']) && class_exists(Package::class)) { + $v['assets'] ??= []; + } return $v; }) ->end() - ->fixXmlConfig('enabled_locale') - ->fixXmlConfig('trusted_header') ->children() ->scalarNode('secret')->end() ->booleanNode('http_method_override') - ->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. Note: When using the HttpCache, you need to call the method in your front controller instead.") + ->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests.") ->defaultFalse() ->end() + ->arrayNode('allowed_http_method_override') + ->info('Sets the list of HTTP methods that can be overridden. Set to null to allow all methods to be overridden (default). Set to an empty array to disallow overrides entirely. Otherwise, provide the list of uppercased method names that are allowed.') + ->stringPrototype()->end() + ->defaultNull() + ->validate() + ->ifTrue(static fn ($v) => array_intersect($v, ['GET', 'HEAD', 'CONNECT', 'TRACE'])) + ->thenInvalid('The HTTP methods "GET", "HEAD", "CONNECT", and "TRACE" cannot be overridden.') + ->end() + ->end() ->scalarNode('trust_x_sendfile_type_header') ->info('Set true to enable support for xsendfile in binary file responses.') ->defaultValue('%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%') @@ -108,7 +118,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('Whether to set the Content-Language HTTP header on the Response using the Request locale.') ->defaultFalse() ->end() - ->arrayNode('enabled_locales') + ->arrayNode('enabled_locales', 'enabled_locale') ->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() @@ -119,12 +129,12 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->variableNode('trusted_proxies') ->beforeNormalization() - ->ifTrue(fn ($v) => 'private_ranges' === $v || 'PRIVATE_SUBNETS' === $v) - ->then(fn () => IpUtils::PRIVATE_SUBNETS) + ->ifTrue(static fn ($v) => 'private_ranges' === $v || 'PRIVATE_SUBNETS' === $v) + ->then(static fn () => IpUtils::PRIVATE_SUBNETS) ->end() ->defaultValue(['%env(default::SYMFONY_TRUSTED_PROXIES)%']) ->end() - ->arrayNode('trusted_headers') + ->arrayNode('trusted_headers', 'trusted_header') ->performNoDeepMerging() ->beforeNormalization()->ifString()->then(static fn ($v) => $v ? [$v] : [])->end() ->prototype('scalar')->end() @@ -144,7 +154,7 @@ public function getConfigTreeBuilder(): TreeBuilder return ContainerBuilder::willBeAvailable($package, $class, $parentPackages); }; - $enableIfStandalone = fn (string $package, string $class) => !class_exists(FullStack::class) && $willBeAvailable($package, $class) ? 'canBeDisabled' : 'canBeEnabled'; + $enableIfStandalone = static fn (string $package, string $class) => !class_exists(FullStack::class) && $willBeAvailable($package, $class) ? 'canBeDisabled' : 'canBeEnabled'; $this->addCsrfSection($rootNode); $this->addFormSection($rootNode, $enableIfStandalone); @@ -214,11 +224,10 @@ private function addCsrfSection(ArrayNodeDefinition $rootNode): void ->treatTrueLike(['enabled' => true]) ->treatNullLike(['enabled' => true]) ->addDefaultsIfNotSet() - ->fixXmlConfig('stateless_token_id') ->children() // defaults to (framework.csrf_protection.stateless_token_ids || framework.session.enabled) && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class) ->scalarNode('enabled')->defaultNull()->end() - ->arrayNode('stateless_token_ids') + ->arrayNode('stateless_token_ids', 'stateless_token_id') ->scalarPrototype()->end() ->info('Enable headers/cookies-based CSRF validation for the listed token ids.') ->end() @@ -275,8 +284,6 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void ->arrayNode('http_cache') ->info('HTTP cache configuration') ->canBeEnabled() - ->fixXmlConfig('private_header') - ->fixXmlConfig('skip_response_header') ->children() ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() ->enumNode('trace_level') @@ -284,11 +291,11 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void ->end() ->scalarNode('trace_header')->end() ->integerNode('default_ttl')->end() - ->arrayNode('private_headers') + ->arrayNode('private_headers', 'private_header') ->performNoDeepMerging() ->scalarPrototype()->end() ->end() - ->arrayNode('skip_response_headers') + ->arrayNode('skip_response_headers', 'skip_response_header') ->performNoDeepMerging() ->scalarPrototype()->end() ->end() @@ -365,13 +372,13 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode): void private function addWorkflowSection(ArrayNodeDefinition $rootNode): void { $rootNode - ->fixXmlConfig('workflow') ->children() - ->arrayNode('workflows') + ->arrayNode('workflows', 'workflow') ->canBeEnabled() ->beforeNormalization() - ->always(static function ($v) { - if (\is_array($v) && true === $v['enabled']) { + ->ifArray() + ->then(static function ($v) { + if (true === ($v['enabled'] ?? false)) { $workflows = $v; unset($workflows['enabled']); @@ -401,14 +408,9 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void }) ->end() ->children() - ->arrayNode('workflows') + ->arrayNode('workflows', 'workflow') ->useAttributeAsKey('name') ->prototype('array') - ->fixXmlConfig('support') - ->fixXmlConfig('definition_validator') - ->fixXmlConfig('place') - ->fixXmlConfig('transition') - ->fixXmlConfig('event_to_dispatch', 'events_to_dispatch') ->children() ->arrayNode('audit_trail') ->canBeEnabled() @@ -430,8 +432,8 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() - ->arrayNode('supports') - ->beforeNormalization()->castToArray()->end() + ->arrayNode('supports', 'support') + ->acceptAndWrap(['string']) ->prototype('scalar') ->cannotBeEmpty() ->validate() @@ -440,7 +442,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() - ->arrayNode('definition_validators') + ->arrayNode('definition_validators', 'definition_validator') ->prototype('scalar') ->cannotBeEmpty() ->validate() @@ -461,26 +463,21 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->cannotBeEmpty() ->end() ->arrayNode('initial_marking') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->defaultValue([]) ->prototype('scalar')->end() ->end() - ->variableNode('events_to_dispatch') - ->defaultValue(null) + ->arrayNode('events_to_dispatch', 'event_to_dispatch') + ->defaultNull() + ->stringPrototype()->end() ->validate() ->ifTrue(static function ($v) { - if (null === $v) { + if (!class_exists(WorkflowEvents::class)) { 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, true)) { + if (!\in_array($value, WorkflowEvents::ALIASES, true)) { return true; } } @@ -492,17 +489,38 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->info('Select which Transition events should be dispatched for this Workflow.') ->example(['workflow.enter', 'workflow.transition']) ->end() - ->arrayNode('places') + ->arrayNode('places', 'place') ->beforeNormalization() - ->always() + ->ifString() ->then(static function ($places) { - if (!\is_array($places)) { - throw new InvalidConfigurationException('The "places" option must be an array in workflow configuration.'); + if (2 !== \count($places = explode('::', $places, 2))) { + throw new InvalidConfigurationException('The "places" option must be a "FQCN::glob" pattern in workflow configuration.'); + } + [$class, $pattern] = $places; + if (!class_exists($class) && !interface_exists($class, false)) { + throw new InvalidConfigurationException(\sprintf('The "places" option must be a "FQCN::glob" pattern in workflow configuration, but class "%s" is not found.', $class)); } + $places = []; + $regex = Glob::toRegex($pattern, false); + + foreach ((new \ReflectionClass($class))->getConstants() as $name => $value) { + if (preg_match($regex, $name)) { + $places[] = $value; + } + } + + return $places ?: throw new InvalidConfigurationException(\sprintf('No places found for pattern "%s::%s" in workflow configuration.', $class, $pattern)); + }) + ->end() + ->beforeNormalization() + ->ifArray() + ->then(static function ($places) { $normalizedPlaces = []; foreach ($places as $key => $value) { - if (!\is_array($value)) { + if ($value instanceof \BackedEnum) { + $value = ['name' => $value->value]; + } elseif (!\is_array($value)) { $value = ['name' => $value]; } $value['name'] ??= $key; @@ -528,13 +546,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() - ->arrayNode('transitions') + ->arrayNode('transitions', 'transition') ->beforeNormalization() - ->always(static function ($transitions) { - if (!\is_array($transitions)) { - throw new InvalidConfigurationException('The "transitions" option must be an array in workflow configuration.'); - } - + ->ifArray() + ->then(static function ($transitions) { $normalizedTransitions = []; foreach ($transitions as $key => $transition) { if (\is_array($transition)) { @@ -556,29 +571,95 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->requiresAtLeastOneElement() ->prototype('array') ->children() - ->scalarNode('name') + ->stringNode('name') ->isRequired() ->cannotBeEmpty() ->end() - ->scalarNode('guard') + ->stringNode('guard') ->cannotBeEmpty() ->info('An expression to block the transition.') ->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'') ->end() ->arrayNode('from') ->performNoDeepMerging() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['backed-enum', 'string']) + ->beforeNormalization() + ->ifArray() + ->then($workflowNormalizeArcs = static function ($arcs) { + // Fix XML parsing, when only one arc is defined + if (\array_key_exists('value', $arcs) && \array_key_exists('weight', $arcs)) { + $arcs = [[ + 'place' => $arcs['value'], + 'weight' => $arcs['weight'], + ]]; + } elseif (\array_key_exists('place', $arcs)) { + $arcs = [$arcs]; + } + + $normalizedArcs = []; + foreach ($arcs as $arc) { + if (\is_string($arc) || $arc instanceof \BackedEnum) { + $arc = ['place' => $arc]; + } elseif (!\is_array($arc)) { + throw new InvalidConfigurationException('The "from" arcs must be a list of strings or arrays in workflow configuration.'); + } elseif (\array_key_exists('value', $arc) && \array_key_exists('weight', $arc)) { + // Fix XML parsing + $arc = [ + 'place' => $arc['value'], + 'weight' => $arc['weight'], + ]; + } + + if (($arc['place'] ?? null) instanceof \BackedEnum) { + $arc['place'] = $arc['place']->value; + } + + $normalizedArcs[] = $arc; + } + + return $normalizedArcs; + }) + ->end() ->requiresAtLeastOneElement() - ->prototype('scalar') - ->cannotBeEmpty() + ->prototype('array') + ->children() + ->stringNode('place') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->integerNode('weight') + ->defaultValue(1) + ->min(1) + ->end() + ->end() ->end() ->end() ->arrayNode('to') ->performNoDeepMerging() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['backed-enum', 'string']) + ->beforeNormalization() + ->ifArray() + ->then($workflowNormalizeArcs) + ->end() ->requiresAtLeastOneElement() - ->prototype('scalar') - ->cannotBeEmpty() + ->prototype('array') + ->children() + ->stringNode('place') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->integerNode('weight') + ->defaultValue(1) + ->min(1) + ->end() + ->end() + ->end() + ->end() + ->integerNode('weight') + ->defaultValue(1) + ->validate() + ->ifTrue(static fn ($v) => $v < 1) + ->thenInvalid('The weight must be greater than 0.') ->end() ->end() ->arrayNode('metadata') @@ -600,28 +681,24 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->validate() - ->ifTrue(static function ($v) { - return $v['supports'] && isset($v['support_strategy']); - }) + ->ifTrue(static fn ($v) => $v['supports'] && isset($v['support_strategy'])) ->thenInvalid('"supports" and "support_strategy" cannot be used together.') ->end() ->validate() - ->ifTrue(static function ($v) { - return !$v['supports'] && !isset($v['support_strategy']); - }) + ->ifTrue(static fn ($v) => !$v['supports'] && !isset($v['support_strategy'])) ->thenInvalid('"supports" or "support_strategy" should be configured.') ->end() ->beforeNormalization() - ->always() - ->then(static 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']); - } + ->ifArray() + ->then(static 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 $values; - }) + return $values; + }) ->end() ->end() ->end() @@ -729,16 +806,15 @@ private function addRequestSection(ArrayNodeDefinition $rootNode): void ->arrayNode('request') ->info('Request configuration') ->canBeEnabled() - ->fixXmlConfig('format') ->children() - ->arrayNode('formats') + ->arrayNode('formats', 'format') ->useAttributeAsKey('name') ->prototype('array') + ->acceptAndWrap(['string']) ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && isset($v['mime_type'])) - ->then(fn ($v) => $v['mime_type']) + ->ifArray() + ->then(static fn ($v) => (array) ($v['mime_type'] ?? $v)) ->end() - ->beforeNormalization()->castToArray()->end() ->prototype('scalar')->end() ->end() ->end() @@ -755,7 +831,6 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->arrayNode('assets') ->info('Assets configuration') ->{$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.') @@ -766,9 +841,9 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->scalarNode('version_format')->defaultValue('%%s?%%s')->end() ->scalarNode('json_manifest_path')->defaultNull()->end() ->scalarNode('base_path')->defaultValue('')->end() - ->arrayNode('base_urls') + ->arrayNode('base_urls', 'base_url') ->requiresAtLeastOneElement() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->end() ->end() @@ -790,13 +865,11 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl }) ->thenInvalid('You cannot use both "version" and "json_manifest_path" at the same time under "assets".') ->end() - ->fixXmlConfig('package') ->children() - ->arrayNode('packages') + ->arrayNode('packages', 'package') ->normalizeKeys(false) ->useAttributeAsKey('name') ->prototype('array') - ->fixXmlConfig('base_url') ->children() ->booleanNode('strict_mode') ->info('Throw an exception if an entry is missing from the manifest.json.') @@ -805,16 +878,16 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->scalarNode('version_strategy')->defaultNull()->end() ->scalarNode('version') ->beforeNormalization() - ->ifTrue(fn ($v) => '' === $v) - ->then(fn () => null) + ->ifString() + ->then(static fn ($v) => '' === $v ? null : $v) ->end() ->end() ->scalarNode('version_format')->defaultNull()->end() ->scalarNode('json_manifest_path')->defaultNull()->end() ->scalarNode('base_path')->defaultValue('')->end() - ->arrayNode('base_urls') + ->arrayNode('base_urls', 'base_url') ->requiresAtLeastOneElement() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->end() ->end() @@ -851,20 +924,17 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->arrayNode('asset_mapper') ->info('Asset Mapper configuration') ->{$enableIfStandalone('symfony/asset-mapper', AssetMapper::class)}() - ->fixXmlConfig('path') - ->fixXmlConfig('excluded_pattern') - ->fixXmlConfig('extension') - ->fixXmlConfig('importmap_script_attribute') ->children() // add array node called "paths" that will be an array of strings - ->arrayNode('paths') + ->arrayNode('paths', 'path') ->info('Directories that hold assets that should be in the mapper. Can be a simple array of an array of ["path/to/assets": "namespace"].') ->example(['assets/']) ->normalizeKeys(false) ->useAttributeAsKey('namespace') + ->acceptAndWrap(['string']) ->beforeNormalization() - ->always() - ->then(function ($v) { + ->ifArray() + ->then(static function ($v) { $result = []; foreach ($v as $key => $item) { // "dir" => "namespace" @@ -888,7 +958,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->end() ->prototype('scalar')->end() ->end() - ->arrayNode('excluded_patterns') + ->arrayNode('excluded_patterns', 'excluded_pattern') ->info('Array of glob patterns of asset file paths that should not be in the asset mapper.') ->prototype('scalar')->end() ->example(['*/assets/build/*', '*/*_.scss']) @@ -911,7 +981,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->info('Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import \'./non-existent.js\'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is.') ->defaultValue('warn') ->end() - ->arrayNode('extensions') + ->arrayNode('extensions', 'extension') ->info('Key-value pair of file extensions set to their mime type.') ->normalizeKeys(false) ->useAttributeAsKey('extension') @@ -930,7 +1000,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->end() ->defaultValue('es-module-shims') ->end() - ->arrayNode('importmap_script_attributes') + ->arrayNode('importmap_script_attributes', 'importmap_script_attribute') ->info('Key-value pair of attributes to add to script tags output for the importmap.') ->normalizeKeys(false) ->useAttributeAsKey('key') @@ -944,19 +1014,17 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->arrayNode('precompress') ->info('Precompress assets with Brotli, Zstandard and gzip.') ->canBeEnabled() - ->fixXmlConfig('format') - ->fixXmlConfig('extension') ->children() - ->arrayNode('formats') + ->arrayNode('formats', 'format') ->info('Array of formats to enable. "brotli", "zstandard" and "gzip" are supported. Defaults to all formats supported by the system. The entire list must be provided.') ->prototype('scalar')->end() ->performNoDeepMerging() ->validate() - ->ifTrue(static fn (array $v) => array_diff($v, ['brotli', 'zstandard', 'gzip'])) + ->ifTrue(static fn ($v) => array_diff($v, ['brotli', 'zstandard', 'gzip'])) ->thenInvalid('Unsupported format: "brotli", "zstandard" and "gzip" are supported.') ->end() ->end() - ->arrayNode('extensions') + ->arrayNode('extensions', 'extension') ->info('Array of extensions to compress. The entire list must be provided, no merging occurs.') ->prototype('scalar')->end() ->performNoDeepMerging() @@ -977,30 +1045,25 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->arrayNode('translator') ->info('Translator configuration') ->{$enableIfStandalone('symfony/translation', Translator::class)}() - ->fixXmlConfig('fallback') - ->fixXmlConfig('path') - ->fixXmlConfig('provider') - ->fixXmlConfig('global') ->children() - ->arrayNode('fallbacks') + ->arrayNode('fallbacks', 'fallback') ->info('Defaults to the value of "default_locale".') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->defaultValue([]) ->end() - ->booleanNode('logging')->defaultValue(false)->end() + ->booleanNode('logging')->defaultFalse()->end() ->scalarNode('formatter')->defaultValue('translator.formatter.default')->end() ->scalarNode('cache_dir')->defaultValue('%kernel.cache_dir%/translations')->end() ->scalarNode('default_path') ->info('The default path used to load translations.') ->defaultValue('%kernel.project_dir%/translations') ->end() - ->arrayNode('paths') + ->arrayNode('paths', 'path') ->prototype('scalar')->end() ->end() ->arrayNode('pseudo_localization') ->canBeEnabled() - ->fixXmlConfig('localizable_html_attribute') ->children() ->booleanNode('accents')->defaultTrue()->end() ->floatNode('expansion_factor') @@ -1009,24 +1072,22 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->booleanNode('brackets')->defaultTrue()->end() ->booleanNode('parse_html')->defaultFalse()->end() - ->arrayNode('localizable_html_attributes') + ->arrayNode('localizable_html_attributes', 'localizable_html_attribute') ->prototype('scalar')->end() ->end() ->end() ->end() - ->arrayNode('providers') + ->arrayNode('providers', 'provider') ->info('Translation providers you can read/write your translations from.') ->useAttributeAsKey('name') ->prototype('array') - ->fixXmlConfig('domain') - ->fixXmlConfig('locale') ->children() ->scalarNode('dsn')->end() - ->arrayNode('domains') + ->arrayNode('domains', 'domain') ->prototype('scalar')->end() ->defaultValue([]) ->end() - ->arrayNode('locales') + ->arrayNode('locales', 'locale') ->prototype('scalar')->end() ->defaultValue([]) ->info('If not set, all locales listed under framework.enabled_locales are used.') @@ -1035,27 +1096,23 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->defaultValue([]) ->end() - ->arrayNode('globals') + ->arrayNode('globals', 'global') ->info('Global parameters.') ->example(['app_version' => 3.14]) ->normalizeKeys(false) ->useAttributeAsKey('name') ->arrayPrototype() - ->fixXmlConfig('parameter') + ->acceptAndWrap(['string'], 'value') ->children() ->variableNode('value')->end() ->stringNode('message')->end() - ->arrayNode('parameters') + ->arrayNode('parameters', 'parameter') ->normalizeKeys(false) ->useAttributeAsKey('name') ->scalarPrototype()->end() ->end() ->stringNode('domain')->end() ->end() - ->beforeNormalization() - ->ifTrue(static fn ($v) => !\is_array($v)) - ->then(static fn ($v) => ['value' => $v]) - ->end() ->validate() ->ifTrue(static fn ($v) => !(isset($v['value']) xor isset($v['message']))) ->thenInvalid('The "globals" parameter should be either a string or an array with a "value" or a "message" key') @@ -1081,18 +1138,17 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->booleanNode('enable_attributes')->{class_exists(FullStack::class) ? 'defaultFalse' : 'defaultTrue'}()->end() ->arrayNode('static_method') + ->acceptAndWrap(['string']) ->defaultValue(['loadValidatorMetadata']) ->prototype('scalar')->end() ->treatFalseLike([]) - ->validate()->castToArray()->end() ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() ->enumNode('email_validation_mode')->values(['html5', 'html5-allow-no-tld', 'strict', 'loose'])->defaultValue('html5')->end() ->arrayNode('mapping') ->addDefaultsIfNotSet() - ->fixXmlConfig('path') ->children() - ->arrayNode('paths') + ->arrayNode('paths', 'path') ->prototype('scalar')->end() ->end() ->end() @@ -1119,7 +1175,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->normalizeKeys(false) ->beforeNormalization() ->ifArray() - ->then(function (array $values): array { + ->then(static function ($values) { foreach ($values as $k => $v) { if (isset($v['service'])) { continue; @@ -1145,9 +1201,8 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e }) ->end() ->arrayPrototype() - ->fixXmlConfig('service') ->children() - ->arrayNode('services') + ->arrayNode('services', 'service') ->prototype('scalar')->end() ->end() ->end() @@ -1166,7 +1221,7 @@ private function addAnnotationsSection(ArrayNodeDefinition $rootNode): void ->arrayNode('annotations') ->canBeEnabled() ->validate() - ->ifTrue(static fn (array $v) => $v['enabled']) + ->ifTrue(static fn ($v) => $v['enabled']) ->thenInvalid('Enabling the doctrine/annotations integration is not supported anymore.') ->end() ->end() @@ -1190,7 +1245,6 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->children() ->arrayNode('serializer') ->info('Serializer configuration') - ->fixXmlConfig('named_serializer', 'named_serializers') ->{$enableIfStandalone('symfony/serializer', Serializer::class)}() ->children() ->booleanNode('enable_attributes')->{class_exists(FullStack::class) ? 'defaultFalse' : 'defaultTrue'}()->end() @@ -1199,15 +1253,14 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->scalarNode('max_depth_handler')->end() ->arrayNode('mapping') ->addDefaultsIfNotSet() - ->fixXmlConfig('path') ->children() - ->arrayNode('paths') + ->arrayNode('paths', 'path') ->prototype('scalar')->end() ->end() ->end() ->end() ->append($defaultContextNode()) - ->arrayNode('named_serializers') + ->arrayNode('named_serializers', 'named_serializer') ->useAttributeAsKey('name') ->arrayPrototype() ->children() @@ -1296,6 +1349,16 @@ private function addTypeInfoSection(ArrayNodeDefinition $rootNode, callable $ena ->arrayNode('type_info') ->info('Type info configuration') ->{$enableIfStandalone('symfony/type-info', Type::class)}() + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('aliases', 'alias') + ->info('Additional type aliases to be used during type context creation.') + ->defaultValue([]) + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->scalarPrototype()->end() + ->end() + ->end() ->end() ->end() ; @@ -1308,7 +1371,6 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe ->arrayNode('cache') ->info('Cache configuration') ->addDefaultsIfNotSet() - ->fixXmlConfig('pool') ->children() ->scalarNode('prefix_seed') ->info('Used to namespace cache keys when using several apps with the same shared backend.') @@ -1330,21 +1392,21 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe ->scalarNode('default_memcached_provider')->defaultValue('memcached://localhost')->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') + ->arrayNode('pools', 'pool') ->useAttributeAsKey('name') ->prototype('array') - ->fixXmlConfig('adapter') - ->beforeNormalization() - ->ifTrue(fn ($v) => isset($v['provider']) && \is_array($v['adapters'] ?? $v['adapter'] ?? null) && 1 < \count($v['adapters'] ?? $v['adapter'])) + ->validate() + ->ifTrue(static fn ($v) => isset($v['provider']) && 1 < \count($v['adapters'])) ->thenInvalid('Pool cannot have a "provider" while more than one adapter is defined') ->end() ->children() - ->arrayNode('adapters') + ->arrayNode('adapters', 'adapter') ->performNoDeepMerging() ->info('One or more adapters to chain for creating the pool, defaults to "cache.app".') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->beforeNormalization() - ->always()->then(function ($values) { + ->ifArray() + ->then(static function ($values) { if ([0] === array_keys($values) && \is_array($values[0])) { return $values[0]; } @@ -1444,9 +1506,8 @@ private function addExceptionsSection(ArrayNodeDefinition $rootNode): void $logLevels = (new \ReflectionClass(LogLevel::class))->getConstants(); $rootNode - ->fixXmlConfig('exception') ->children() - ->arrayNode('exceptions') + ->arrayNode('exceptions', 'exception') ->info('Exception handling configuration') ->useAttributeAsKey('class') ->prototype('array') @@ -1488,40 +1549,40 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->children() ->arrayNode('lock') ->info('Lock configuration') + ->acceptAndWrap(['string'], 'resources') ->{$enableIfStandalone('symfony/lock', Lock::class)}() ->beforeNormalization() - ->ifString()->then(fn ($v) => ['enabled' => true, 'resources' => $v]) - ->end() - ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && !isset($v['enabled'])) - ->then(fn ($v) => $v + ['enabled' => true]) - ->end() - ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && !isset($v['resources']) && !isset($v['resource'])) - ->then(function ($v) { - $e = $v['enabled']; - unset($v['enabled']); + ->ifArray() + ->then(static function ($v) { + if (!isset($v['resources']) && !isset($v['resource'])) { + $v = ['resources' => $v]; + if (\array_key_exists('enabled', $v['resources'])) { + $v['enabled'] = $v['resources']['enabled']; + unset($v['resources']['enabled']); + } + } - return ['enabled' => $e, 'resources' => $v]; + return $v; }) ->end() ->addDefaultsIfNotSet() ->validate() - ->ifTrue(fn ($config) => $config['enabled'] && !$config['resources']) + ->ifTrue(static fn ($v) => $v['enabled'] && !$v['resources']) ->thenInvalid('At least one resource must be defined.') ->end() - ->fixXmlConfig('resource') ->children() - ->arrayNode('resources') + ->arrayNode('resources', 'resource') ->normalizeKeys(false) ->useAttributeAsKey('name') ->defaultValue(['default' => [class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock']]) + ->acceptAndWrap(['string'], 'default') ->beforeNormalization() - ->ifString()->then(fn ($v) => ['default' => $v]) - ->end() - ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && array_is_list($v)) - ->then(function ($v) { + ->ifArray() + ->then(static function ($v) { + if (!array_is_list($v)) { + return $v; + } + $resources = []; foreach ($v as $resource) { $resources[] = \is_array($resource) && isset($resource['name']) @@ -1535,7 +1596,10 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->end() ->prototype('array') ->performNoDeepMerging() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) + // acceptAndWrap() doesn't list null as an accepted value on purpose, + // yet the XML loader can yield some and we should convert them to 'null' + ->beforeNormalization()->ifNull()->then(static fn () => ['null'])->end() ->prototype('scalar')->end() ->end() ->end() @@ -1551,36 +1615,36 @@ private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $en ->children() ->arrayNode('semaphore') ->info('Semaphore configuration') + ->acceptAndWrap(['string'], 'resources') ->{$enableIfStandalone('symfony/semaphore', Semaphore::class)}() ->beforeNormalization() - ->ifString()->then(fn ($v) => ['enabled' => true, 'resources' => $v]) - ->end() - ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && !isset($v['enabled'])) - ->then(fn ($v) => $v + ['enabled' => true]) - ->end() - ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && !isset($v['resources']) && !isset($v['resource'])) - ->then(function ($v) { - $e = $v['enabled']; - unset($v['enabled']); + ->ifArray() + ->then(static function ($v) { + if (!isset($v['resources']) && !isset($v['resource'])) { + $v = ['resources' => $v]; + if (\array_key_exists('enabled', $v['resources'])) { + $v['enabled'] = $v['resources']['enabled']; + unset($v['resources']['enabled']); + } + } - return ['enabled' => $e, 'resources' => $v]; + return $v; }) ->end() ->addDefaultsIfNotSet() - ->fixXmlConfig('resource') ->children() - ->arrayNode('resources') + ->arrayNode('resources', 'resource') ->normalizeKeys(false) ->useAttributeAsKey('name') ->requiresAtLeastOneElement() + ->acceptAndWrap(['string'], 'default') ->beforeNormalization() - ->ifString()->then(fn ($v) => ['default' => $v]) - ->end() - ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && array_is_list($v)) - ->then(function ($v) { + ->ifArray() + ->then(static function ($v) { + if (!array_is_list($v)) { + return $v; + } + $resources = []; foreach ($v as $resource) { $resources[] = \is_array($resource) && isset($resource['name']) @@ -1619,9 +1683,6 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode, callable $en ->arrayNode('messenger') ->info('Messenger configuration') ->{$enableIfStandalone('symfony/messenger', MessageBusInterface::class)}() - ->fixXmlConfig('transport') - ->fixXmlConfig('bus', 'buses') - ->fixXmlConfig('stop_worker_on_signal') ->validate() ->ifTrue(fn ($v) => isset($v['buses']) && \count($v['buses']) > 1 && null === $v['default_bus']) ->thenInvalid('You must specify the "default_bus" if you define more than one bus.') @@ -1635,11 +1696,8 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode, callable $en ->normalizeKeys(false) ->useAttributeAsKey('message_class') ->beforeNormalization() - ->always() + ->ifArray() ->then(function ($config) { - if (!\is_array($config)) { - return []; - } // If XML config with only one routing attribute if (2 === \count($config) && isset($config['message-class']) && isset($config['sender'])) { $config = [0 => $config]; @@ -1696,21 +1754,15 @@ function ($a) { ->end() ->end() ->end() - ->arrayNode('transports') + ->arrayNode('transports', 'transport') ->normalizeKeys(false) ->useAttributeAsKey('name') ->arrayPrototype() - ->beforeNormalization() - ->ifString() - ->then(function (string $dsn) { - return ['dsn' => $dsn]; - }) - ->end() - ->fixXmlConfig('option') + ->acceptAndWrap(['string'], 'dsn') ->children() ->scalarNode('dsn')->end() ->scalarNode('serializer')->defaultNull()->info('Service id of a custom serializer to use.')->end() - ->arrayNode('options') + ->arrayNode('options', 'option') ->normalizeKeys(false) ->defaultValue([]) ->prototype('variable') @@ -1722,8 +1774,10 @@ function ($a) { ->end() ->arrayNode('retry_strategy') ->addDefaultsIfNotSet() + ->acceptAndWrap(['string'], 'service') ->beforeNormalization() - ->always(function ($v) { + ->ifArray() + ->then(static function ($v) { if (isset($v['service']) && (isset($v['max_retries']) || isset($v['delay']) || isset($v['multiplier']) || isset($v['max_delay']))) { throw new \InvalidArgumentException('The "service" cannot be used along with the other "retry_strategy" options.'); } @@ -1751,15 +1805,13 @@ function ($a) { ->defaultNull() ->info('Transport name to send failed messages to (after all retries have failed).') ->end() - ->arrayNode('stop_worker_on_signals') + ->arrayNode('stop_worker_on_signals', 'stop_worker_on_signal') ->defaultValue([]) ->info('A list of signals that should stop the worker; defaults to SIGTERM and SIGINT.') + ->acceptAndWrap(['int', 'string']) ->beforeNormalization() - ->always(function ($signals) { - if (!\is_array($signals)) { - throw new InvalidConfigurationException('The "stop_worker_on_signals" option must be an array in messenger configuration.'); - } - + ->ifArray() + ->then(static function ($signals) { return array_map(static function ($v) { if (\is_string($v) && str_starts_with($v, 'SIG') && \array_key_exists($v, get_defined_constants(true)['pcntl'])) { return \constant($v); @@ -1776,7 +1828,7 @@ function ($a) { ->scalarPrototype()->end() ->end() ->scalarNode('default_bus')->defaultNull()->end() - ->arrayNode('buses') + ->arrayNode('buses', 'bus') ->defaultValue(['messenger.bus.default' => ['default_middleware' => ['enabled' => true, 'allow_no_handlers' => false, 'allow_no_senders' => true], 'middleware' => []]]) ->normalizeKeys(false) ->useAttributeAsKey('name') @@ -1785,13 +1837,14 @@ function ($a) { ->children() ->arrayNode('default_middleware') ->beforeNormalization() - ->ifTrue(fn ($v) => \is_string($v) || \is_bool($v)) - ->then(fn ($v) => [ - 'enabled' => 'allow_no_handlers' === $v ? true : $v, + ->ifString() + ->then(static fn ($v) => [ + 'enabled' => 'allow_no_handlers' === $v, 'allow_no_handlers' => 'allow_no_handlers' === $v, - 'allow_no_senders' => true, ]) ->end() + ->beforeNormalization()->ifTrue()->then(static fn () => ['enabled' => true])->end() + ->beforeNormalization()->ifFalse()->then(static fn () => ['enabled' => false])->end() ->canBeDisabled() ->children() ->booleanNode('allow_no_handlers')->defaultFalse()->end() @@ -1800,18 +1853,17 @@ function ($a) { ->end() ->arrayNode('middleware') ->performNoDeepMerging() + ->acceptAndWrap(['string']) ->beforeNormalization() - ->ifTrue(fn ($v) => \is_string($v) || (\is_array($v) && !\is_int(key($v)))) - ->then(fn ($v) => [$v]) + ->ifArray() + ->then(static fn ($v) => \is_string(key($v)) ? [$v] : $v) ->end() ->defaultValue([]) ->arrayPrototype() + ->acceptAndWrap(['string'], 'id') ->beforeNormalization() - ->always() - ->then(function ($middleware): array { - if (!\is_array($middleware)) { - return ['id' => $middleware]; - } + ->ifArray() + ->then(static function ($middleware): array { if (isset($middleware['id'])) { return $middleware; } @@ -1825,10 +1877,9 @@ function ($a) { ]; }) ->end() - ->fixXmlConfig('argument') ->children() ->scalarNode('id')->isRequired()->cannotBeEmpty()->end() - ->arrayNode('arguments') + ->arrayNode('arguments', 'argument') ->normalizeKeys(false) ->defaultValue([]) ->prototype('variable') @@ -1877,10 +1928,10 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->arrayNode('http_client') ->info('HTTP Client configuration') ->{$enableIfStandalone('symfony/http-client', HttpClient::class)}() - ->fixXmlConfig('scoped_client') ->beforeNormalization() - ->always(function ($config) { - if (empty($config['scoped_clients'])) { + ->ifArray() + ->then(static function ($config) { + if (!($config['scoped_clients'] ?? false)) { return $config; } @@ -1917,15 +1968,14 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('The maximum number of connections to a single host.') ->end() ->arrayNode('default_options') - ->fixXmlConfig('header') ->children() - ->arrayNode('headers') + ->arrayNode('headers', 'header') ->info('Associative array: header => value(s).') ->useAttributeAsKey('name') ->normalizeKeys(false) ->variablePrototype()->end() ->end() - ->arrayNode('vars') + ->arrayNode('vars', 'var') ->info('Associative array: the default vars used to expand the templated URI.') ->normalizeKeys(false) ->variablePrototype()->end() @@ -1940,10 +1990,8 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('Associative array: domain => IP.') ->useAttributeAsKey('host') ->beforeNormalization() - ->always(function ($config) { - if (!\is_array($config)) { - return []; - } + ->ifArray() + ->then(static function ($config) { if (!isset($config['host'], $config['value']) || \count($config) > 2) { return $config; } @@ -2014,33 +2062,28 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->defaultNull() ->info('Rate limiter name to use for throttling requests.') ->end() + ->append($this->createHttpClientCachingSection()) ->append($this->createHttpClientRetrySection()) ->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') + ->arrayNode('scoped_clients', 'scoped_client') ->useAttributeAsKey('name') ->normalizeKeys(false) ->arrayPrototype() - ->fixXmlConfig('header') - ->beforeNormalization() - ->always() - ->then(function ($config) { - if (!class_exists(HttpClient::class)) { - throw new LogicException('HttpClient support cannot be enabled as the component is not installed. Try running "composer require symfony/http-client".'); - } - - return \is_array($config) ? $config : ['base_uri' => $config]; - }) + ->acceptAndWrap(['string'], 'base_uri') + ->validate() + ->ifTrue(static fn () => !class_exists(HttpClient::class)) + ->then(static fn () => 'HttpClient support cannot be enabled as the component is not installed. Try running "composer require symfony/http-client".') ->end() ->validate() - ->ifTrue(fn ($v) => !isset($v['scope']) && !isset($v['base_uri'])) + ->ifTrue(static fn ($v) => !isset($v['scope']) && !isset($v['base_uri'])) ->thenInvalid('Either "scope" or "base_uri" should be defined.') ->end() ->validate() - ->ifTrue(fn ($v) => !empty($v['query']) && !isset($v['base_uri'])) + ->ifTrue(static fn ($v) => !empty($v['query']) && !isset($v['base_uri'])) ->thenInvalid('"query" applies to "base_uri" but no base URI is defined.') ->end() ->children() @@ -2065,10 +2108,8 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('Associative array of query string values merged with the base URI.') ->useAttributeAsKey('key') ->beforeNormalization() - ->always(function ($config) { - if (!\is_array($config)) { - return []; - } + ->ifArray() + ->then(static function ($config) { if (!isset($config['key'], $config['value']) || \count($config) > 2) { return $config; } @@ -2079,7 +2120,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->normalizeKeys(false) ->scalarPrototype()->end() ->end() - ->arrayNode('headers') + ->arrayNode('headers', 'header') ->info('Associative array: header => value(s).') ->useAttributeAsKey('name') ->normalizeKeys(false) @@ -2095,10 +2136,8 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('Associative array: domain => IP.') ->useAttributeAsKey('host') ->beforeNormalization() - ->always(function ($config) { - if (!\is_array($config)) { - return []; - } + ->ifArray() + ->then(static function ($config) { if (!isset($config['host'], $config['value']) || \count($config) > 2) { return $config; } @@ -2169,6 +2208,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->defaultNull() ->info('Rate limiter name to use for throttling requests.') ->end() + ->append($this->createHttpClientCachingSection()) ->append($this->createHttpClientRetrySection()) ->end() ->end() @@ -2179,17 +2219,44 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ; } + private function createHttpClientCachingSection(): ArrayNodeDefinition + { + $root = new NodeBuilder(); + + return $root + ->arrayNode('caching') + ->info('Caching configuration.') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->stringNode('cache_pool') + ->info('The taggable cache pool to use for storing the responses.') + ->defaultValue('cache.http_client') + ->cannotBeEmpty() + ->end() + ->booleanNode('shared') + ->info('Indicates whether the cache is shared (public) or private.') + ->defaultTrue() + ->end() + ->integerNode('max_ttl') + ->info('The maximum TTL (in seconds) allowed for cached responses. Null means no cap.') + ->defaultNull() + ->min(0) + ->end() + ->end(); + } + private function createHttpClientRetrySection(): ArrayNodeDefinition { $root = new NodeBuilder(); return $root ->arrayNode('retry_failed') - ->fixXmlConfig('http_code') ->canBeEnabled() ->addDefaultsIfNotSet() ->beforeNormalization() - ->always(function ($v) { + ->ifArray() + ->then(static 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.'); } @@ -2199,8 +2266,9 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition ->end() ->children() ->scalarNode('retry_strategy')->defaultNull()->info('service id to override the retry strategy.')->end() - ->arrayNode('http_codes') + ->arrayNode('http_codes', 'http_code') ->performNoDeepMerging() + ->acceptAndWrap(['int', 'string']) ->beforeNormalization() ->ifArray() ->then(static function ($v) { @@ -2224,15 +2292,15 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition ->end() ->useAttributeAsKey('code') ->arrayPrototype() - ->fixXmlConfig('method') ->children() ->integerNode('code')->end() - ->arrayNode('methods') + ->arrayNode('methods', 'method') + ->acceptAndWrap(['string']) ->beforeNormalization() ->ifArray() - ->then(fn ($v) => array_map('strtoupper', $v)) + ->then(static fn ($v) => array_map('strtoupper', $v)) ->end() - ->prototype('scalar')->end() + ->stringPrototype()->end() ->info('A list of HTTP methods that triggers a retry for this status code. When empty, all methods are retried.') ->end() ->end() @@ -2259,49 +2327,48 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->ifTrue(fn ($v) => 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') + ->arrayNode('transports', 'transport') ->useAttributeAsKey('name') ->prototype('scalar')->end() ->end() ->arrayNode('envelope') ->info('Mailer Envelope configuration') - ->fixXmlConfig('recipient') - ->fixXmlConfig('allowed_recipient') ->children() ->scalarNode('sender')->end() - ->arrayNode('recipients') + ->arrayNode('recipients', 'recipient') ->performNoDeepMerging() + ->acceptAndWrap(['string']) ->beforeNormalization() ->ifArray() - ->then(fn ($v) => array_filter(array_values($v))) + ->then(static fn ($v) => array_values(array_filter($v))) ->end() ->prototype('scalar')->end() ->end() - ->arrayNode('allowed_recipients') + ->arrayNode('allowed_recipients', 'allowed_recipient') ->info('A list of regular expressions that allow recipients when "recipients" option is defined.') ->example(['.*@example\.com']) ->performNoDeepMerging() + ->acceptAndWrap(['string']) ->beforeNormalization() ->ifArray() - ->then(fn ($v) => array_filter(array_values($v))) + ->then(static fn ($v) => array_values(array_filter($v))) ->end() ->prototype('scalar')->end() ->end() ->end() ->end() - ->arrayNode('headers') + ->arrayNode('headers', 'header') ->normalizeKeys(false) ->useAttributeAsKey('name') ->prototype('array') ->normalizeKeys(false) + ->acceptAndWrap(['string'], 'value') ->beforeNormalization() - ->ifTrue(fn ($v) => !\is_array($v) || array_keys($v) !== ['value']) - ->then(fn ($v) => ['value' => $v]) + ->ifArray() + ->then(static fn ($v) => array_keys($v) !== ['value'] ? ['value' => $v] : $v) ->end() ->children() ->variableNode('value')->end() @@ -2310,7 +2377,6 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->end() ->arrayNode('dkim_signer') ->addDefaultsIfNotSet() - ->fixXmlConfig('option') ->canBeEnabled() ->info('DKIM signer configuration') ->children() @@ -2325,7 +2391,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->info('The private key passphrase') ->defaultValue('') ->end() - ->arrayNode('options') + ->arrayNode('options', 'option') ->performNoDeepMerging() ->normalizeKeys(false) ->useAttributeAsKey('name') @@ -2370,10 +2436,8 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->info('A set of algorithms used to encrypt the message') ->defaultNull() ->beforeNormalization() - ->always(function ($v): ?int { - if (null === $v) { - return null; - } + ->ifString() + ->then(static function ($v): ?int { if (\defined('OPENSSL_CIPHER_'.$v)) { return \constant('OPENSSL_CIPHER_'.$v); } @@ -2405,36 +2469,23 @@ private function addNotifierSection(ArrayNodeDefinition $rootNode, callable $ena ->{$enableIfStandalone('symfony/notifier', Notifier::class)}() ->children() ->scalarNode('message_bus')->defaultNull()->info('The message bus to use. Defaults to the default bus if the Messenger component is installed.')->end() - ->end() - ->fixXmlConfig('chatter_transport') - ->children() - ->arrayNode('chatter_transports') + ->arrayNode('chatter_transports', 'chatter_transport') ->useAttributeAsKey('name') ->prototype('scalar')->end() ->end() - ->end() - ->fixXmlConfig('texter_transport') - ->children() - ->arrayNode('texter_transports') + ->arrayNode('texter_transports', 'texter_transport') ->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()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->end() ->end() - ->end() - ->fixXmlConfig('admin_recipient') - ->children() - ->arrayNode('admin_recipients') + ->arrayNode('admin_recipients', 'admin_recipient') ->prototype('array') ->children() ->scalarNode('email')->cannotBeEmpty()->end() @@ -2496,22 +2547,22 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->arrayNode('rate_limiter') ->info('Rate limiter configuration') ->{$enableIfStandalone('symfony/rate-limiter', TokenBucketLimiter::class)}() - ->fixXmlConfig('limiter') ->beforeNormalization() - ->ifTrue(fn ($v) => \is_array($v) && !isset($v['limiters']) && !isset($v['limiter'])) - ->then(function (array $v) { - $newV = [ - 'enabled' => $v['enabled'] ?? true, - ]; - unset($v['enabled']); - - $newV['limiters'] = $v; + ->ifArray() + ->then(static function ($v) { + if (!isset($v['limiters']) && !isset($v['limiter'])) { + $v = ['limiters' => $v]; + if (\array_key_exists('enabled', $v['limiters'])) { + $v['enabled'] = $v['limiters']['enabled']; + unset($v['limiters']['enabled']); + } + } - return $newV; + return $v; }) ->end() ->children() - ->arrayNode('limiters') + ->arrayNode('limiters', 'limiter') ->useAttributeAsKey('name') ->arrayPrototype() ->children() @@ -2532,9 +2583,9 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->isRequired() ->values(['fixed_window', 'token_bucket', 'sliding_window', 'compound', 'no_limit']) ->end() - ->arrayNode('limiters') + ->arrayNode('limiters', 'limiter') ->info('The limiter names to use when using the "compound" policy.') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->scalarPrototype()->end() ->end() ->integerNode('limit') @@ -2554,7 +2605,7 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->end() ->end() ->validate() - ->ifTrue(static fn ($v) => !\in_array($v['policy'], ['no_limit', 'compound']) && !isset($v['limit'])) + ->ifTrue(static fn ($v) => !\in_array($v['policy'], ['no_limit', 'compound'], true) && !isset($v['limit'])) ->thenInvalid('A limit must be provided when using a policy different than "compound" or "no_limit".') ->end() ->end() @@ -2605,23 +2656,10 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->arrayNode('html_sanitizer') ->info('HtmlSanitizer configuration') ->{$enableIfStandalone('symfony/html-sanitizer', HtmlSanitizerInterface::class)}() - ->fixXmlConfig('sanitizer') ->children() - ->arrayNode('sanitizers') + ->arrayNode('sanitizers', 'sanitizer') ->useAttributeAsKey('name') ->arrayPrototype() - ->fixXmlConfig('allow_element') - ->fixXmlConfig('block_element') - ->fixXmlConfig('drop_element') - ->fixXmlConfig('allow_attribute') - ->fixXmlConfig('drop_attribute') - ->fixXmlConfig('force_attribute') - ->fixXmlConfig('allowed_link_scheme') - ->fixXmlConfig('allowed_link_host') - ->fixXmlConfig('allowed_media_scheme') - ->fixXmlConfig('allowed_media_host') - ->fixXmlConfig('with_attribute_sanitizer') - ->fixXmlConfig('without_attribute_sanitizer') ->children() ->booleanNode('allow_safe_elements') ->info('Allows "safe" elements and attributes.') @@ -2631,14 +2669,14 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->info('Allows all static elements and attributes from the W3C Sanitizer API standard.') ->defaultFalse() ->end() - ->arrayNode('allow_elements') + ->arrayNode('allow_elements', 'allow_element') ->info('Configures the elements that the sanitizer should retain from the input. The element name is the key, the value is either a list of allowed attributes for this element or "*" to allow the default set of attributes (https://wicg.github.io/sanitizer-api/#default-configuration).') ->example(['i' => '*', 'a' => ['title'], 'span' => 'class']) ->normalizeKeys(false) ->useAttributeAsKey('name') ->variablePrototype() ->beforeNormalization() - ->ifArray()->then(fn ($n) => $n['attribute'] ?? $n) + ->ifArray()->then(static fn ($n) => $n['attribute'] ?? $n) ->end() ->validate() ->ifTrue(fn ($n): bool => !\is_string($n) && !\is_array($n)) @@ -2646,89 +2684,89 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->end() ->end() ->end() - ->arrayNode('block_elements') + ->arrayNode('block_elements', 'block_element') ->info('Configures elements as blocked. Blocked elements are elements the sanitizer should remove from the input, but retain their children.') - ->beforeNormalization()->castToArray()->end() - ->scalarPrototype()->end() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() - ->arrayNode('drop_elements') + ->arrayNode('drop_elements', 'drop_element') ->info('Configures elements as dropped. Dropped elements are elements the sanitizer should remove from the input, including their children.') - ->beforeNormalization()->castToArray()->end() - ->scalarPrototype()->end() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() - ->arrayNode('allow_attributes') + ->arrayNode('allow_attributes', 'allow_attribute') ->info('Configures attributes as allowed. Allowed attributes are attributes the sanitizer should retain from the input.') ->normalizeKeys(false) ->useAttributeAsKey('name') ->variablePrototype() ->beforeNormalization() - ->ifArray()->then(fn ($n) => $n['element'] ?? $n) + ->ifArray()->then(static fn ($n) => $n['element'] ?? $n) ->end() ->end() ->end() - ->arrayNode('drop_attributes') + ->arrayNode('drop_attributes', 'drop_attribute') ->info('Configures attributes as dropped. Dropped attributes are attributes the sanitizer should remove from the input.') ->normalizeKeys(false) ->useAttributeAsKey('name') ->variablePrototype() ->beforeNormalization() - ->ifArray()->then(fn ($n) => $n['element'] ?? $n) + ->ifArray()->then(static fn ($n) => $n['element'] ?? $n) ->end() ->end() ->end() - ->arrayNode('force_attributes') + ->arrayNode('force_attributes', 'force_attribute') ->info('Forcefully set the values of certain attributes on certain elements.') ->normalizeKeys(false) ->useAttributeAsKey('name') ->arrayPrototype() ->normalizeKeys(false) ->useAttributeAsKey('name') - ->scalarPrototype()->end() + ->stringPrototype()->end() ->end() ->end() ->booleanNode('force_https_urls') ->info('Transforms URLs using the HTTP scheme to use the HTTPS scheme instead.') ->defaultFalse() ->end() - ->arrayNode('allowed_link_schemes') + ->arrayNode('allowed_link_schemes', 'allowed_link_scheme') ->info('Allows only a given list of schemes to be used in links href attributes.') - ->scalarPrototype()->end() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() - ->variableNode('allowed_link_hosts') + ->arrayNode('allowed_link_hosts', 'allowed_link_host') ->info('Allows only a given list of hosts to be used in links href attributes.') - ->defaultValue(null) - ->validate() - ->ifTrue(fn ($v) => !\is_array($v) && null !== $v) - ->thenInvalid('The "allowed_link_hosts" parameter must be an array or null') - ->end() + ->defaultNull() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() ->booleanNode('allow_relative_links') ->info('Allows relative URLs to be used in links href attributes.') ->defaultFalse() ->end() - ->arrayNode('allowed_media_schemes') + ->arrayNode('allowed_media_schemes', 'allowed_media_scheme') ->info('Allows only a given list of schemes to be used in media source attributes (img, audio, video, ...).') - ->scalarPrototype()->end() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() - ->variableNode('allowed_media_hosts') + ->arrayNode('allowed_media_hosts', 'allowed_media_host') ->info('Allows only a given list of hosts to be used in media source attributes (img, audio, video, ...).') - ->defaultValue(null) - ->validate() - ->ifTrue(fn ($v) => !\is_array($v) && null !== $v) - ->thenInvalid('The "allowed_media_hosts" parameter must be an array or null') - ->end() + ->defaultNull() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() ->booleanNode('allow_relative_medias') ->info('Allows relative URLs to be used in media source attributes (img, audio, video, ...).') ->defaultFalse() ->end() - ->arrayNode('with_attribute_sanitizers') + ->arrayNode('with_attribute_sanitizers', 'with_attribute_sanitizer') ->info('Registers custom attribute sanitizers.') - ->scalarPrototype()->end() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() - ->arrayNode('without_attribute_sanitizers') + ->arrayNode('without_attribute_sanitizers', 'without_attribute_sanitizer') ->info('Unregisters custom attribute sanitizers.') - ->scalarPrototype()->end() + ->acceptAndWrap(['string']) + ->stringPrototype()->end() ->end() ->integerNode('max_input_length') ->info('The maximum length allowed for the sanitized input.') diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 56de4975a..a15495373 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -33,6 +33,7 @@ use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface; use Symfony\Bundle\FullStack; use Symfony\Bundle\MercureBundle\MercureBundle; +use Symfony\Component\Asset\Package; use Symfony\Component\Asset\PackageInterface; use Symfony\Component\AssetMapper\AssetMapper; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; @@ -46,6 +47,7 @@ use Symfony\Component\Cache\Marshaller\MarshallerInterface; use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Clock\ClockInterface; +use Symfony\Component\Config\Builder\ConfigBuilderGenerator; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; @@ -59,9 +61,9 @@ use Symfony\Component\Console\Debug\CliRequest; use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\ArgumentTrait; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; -use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; @@ -83,6 +85,7 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Glob; +use Symfony\Component\Form\EnumFormTypeGuesser; use Symfony\Component\Form\Extension\HtmlSanitizer\Type\TextTypeHtmlSanitizerExtension; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormTypeExtensionInterface; @@ -91,6 +94,8 @@ use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; +use Symfony\Component\HttpClient\CachingHttpClient; +use Symfony\Component\HttpClient\Exception\ChunkCacheItemNotFoundException; use Symfony\Component\HttpClient\Messenger\PingWebhookMessageHandler; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Retry\GenericRetryStrategy; @@ -106,6 +111,7 @@ use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; use Symfony\Component\HttpKernel\Profiler\ProfilerStateChecker; use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; @@ -116,6 +122,7 @@ use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\PersistingStoreInterface; +use Symfony\Component\Lock\Serializer\LockKeyNormalizer; use Symfony\Component\Lock\Store\StoreFactory; use Symfony\Component\Mailer\Bridge as MailerBridge; use Symfony\Component\Mailer\Command\MailerTestCommand; @@ -132,8 +139,11 @@ use Symfony\Component\Messenger\Handler\BatchHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Middleware\AddDefaultStampsMiddleware; use Symfony\Component\Messenger\Middleware\DeduplicateMiddleware; use Symfony\Component\Messenger\Middleware\RouterContextMiddleware; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory; +use Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface as MessengerTransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportInterface; @@ -170,6 +180,8 @@ use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; use Symfony\Component\RemoteEvent\RemoteEvent; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Loader\AttributeServicesLoader; +use Symfony\Component\Routing\Loader\XmlFileLoader as RoutingXmlFileLoader; use Symfony\Component\Scheduler\Attribute\AsCronTask; use Symfony\Component\Scheduler\Attribute\AsPeriodicTask; use Symfony\Component\Scheduler\Attribute\AsSchedule; @@ -177,14 +189,17 @@ use Symfony\Component\Scheduler\Messenger\Serializer\Normalizer\SchedulerTriggerNormalizer; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Semaphore\PersistingStoreInterface as SemaphoreStoreInterface; use Symfony\Component\Semaphore\Semaphore; use Symfony\Component\Semaphore\SemaphoreFactory; use Symfony\Component\Semaphore\Store\StoreFactory as SemaphoreStoreFactory; +use Symfony\Component\Serializer\Attribute as SerializerMapping; +use Symfony\Component\Serializer\Attribute\ExtendsSerializationFor; +use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass as SerializerAttributeMetadataPass; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; -use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; @@ -209,16 +224,21 @@ use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Validator\Attribute\ExtendsValidationFor; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\ConstraintValidatorInterface; +use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass as ValidatorAttributeMetadataPass; use Symfony\Component\Validator\GroupProviderInterface; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Validation; use Symfony\Component\Webhook\Controller\WebhookController; +use Symfony\Component\WebLink\HttpHeaderParser; use Symfony\Component\WebLink\HttpHeaderSerializer; use Symfony\Component\Workflow; +use Symfony\Component\Workflow\Arc; use Symfony\Component\Workflow\WorkflowInterface; use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand; use Symfony\Component\Yaml\Yaml; @@ -275,6 +295,12 @@ public function load(array $configs, ContainerBuilder $container): void if (!class_exists(RunProcessMessageHandler::class)) { $container->removeDefinition('process.messenger.process_message_handler'); } + if (!class_exists(IsSignatureValidAttributeListener::class)) { + $container->removeDefinition('controller.is_signature_valid_attribute_listener'); + } + if (!class_exists(ConfigBuilderGenerator::class)) { + $container->removeDefinition('config_builder.warmer'); + } if ($this->hasConsole()) { $loader->load('console.php'); @@ -361,6 +387,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->parameterCannotBeEmpty('kernel.secret', 'A non-empty value for the parameter "kernel.secret" is required. Did you forget to configure the '.$emptySecretHint.'?'); $container->setParameter('kernel.http_method_override', $config['http_method_override']); + $container->setParameter('kernel.allowed_http_method_override', $config['allowed_http_method_override']); $container->setParameter('kernel.trust_x_sendfile_type_header', $config['trust_x_sendfile_type_header']); $container->setParameter('kernel.trusted_hosts', [0] === array_keys($config['trusted_hosts']) ? $config['trusted_hosts'][0] : $config['trusted_hosts']); $container->setParameter('kernel.default_locale', $config['default_locale']); @@ -389,7 +416,7 @@ public function load(array $configs, ContainerBuilder $container): void } if ($this->readConfigEnabled('assets', $container, $config['assets'])) { - if (!class_exists(\Symfony\Component\Asset\Package::class)) { + if (!class_exists(Package::class)) { throw new LogicException('Asset support cannot be enabled as the Asset component is not installed. Try running "composer require symfony/asset".'); } @@ -420,7 +447,7 @@ public function load(array $configs, ContainerBuilder $container): void } $propertyInfoEnabled = $this->readConfigEnabled('property_info', $container, $config['property_info']); - $this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override']); + $this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override'], $config['allowed_http_method_override']); $this->registerEsiConfiguration($config['esi'], $container, $loader); $this->registerSsiConfiguration($config['ssi'], $container, $loader); $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); @@ -465,7 +492,7 @@ public function load(array $configs, ContainerBuilder $container): void } if ($typeInfoEnabled = $this->readConfigEnabled('type_info', $container, $config['type_info'])) { - $this->registerTypeInfoConfiguration($container, $loader); + $this->registerTypeInfoConfiguration($config['type_info'], $container, $loader); } if ($propertyInfoEnabled) { @@ -502,6 +529,11 @@ public function load(array $configs, ContainerBuilder $container): void } $loader->load('web_link.php'); + + // Require symfony/web-link 7.4 + if (!class_exists(HttpHeaderParser::class)) { + $container->removeDefinition('web_link.http_header_parser'); + } } if ($this->readConfigEnabled('uid', $container, $config['uid'])) { @@ -589,9 +621,9 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('cache.messenger.restart_workers_signal'); if ($container->hasDefinition('messenger.transport.amqp.factory') && !class_exists(MessengerBridge\Amqp\Transport\AmqpTransportFactory::class)) { - if (class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class)) { + if (class_exists(AmqpTransportFactory::class)) { $container->getDefinition('messenger.transport.amqp.factory') - ->setClass(\Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory::class) + ->setClass(AmqpTransportFactory::class) ->addTag('messenger.transport_factory'); } else { $container->removeDefinition('messenger.transport.amqp.factory'); @@ -599,9 +631,9 @@ public function load(array $configs, ContainerBuilder $container): void } if ($container->hasDefinition('messenger.transport.redis.factory') && !class_exists(MessengerBridge\Redis\Transport\RedisTransportFactory::class)) { - if (class_exists(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class)) { + if (class_exists(RedisTransportFactory::class)) { $container->getDefinition('messenger.transport.redis.factory') - ->setClass(\Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory::class) + ->setClass(RedisTransportFactory::class) ->addTag('messenger.transport_factory'); } else { $container->removeDefinition('messenger.transport.redis.factory'); @@ -747,19 +779,38 @@ public function load(array $configs, ContainerBuilder $container): void $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)); + + if (!$reflector instanceof \ReflectionMethod) { + $definition->addTag('kernel.event_listener', $tagAttributes); + + return; + } + + 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(); + + if (!$eventArg = $reflector->getParameters()[0] ?? null) { + throw new LogicException(\sprintf('AsEventListener attribute requires the first argument of "%s::%s()" to be an event object.', $reflector->class, $reflector->name)); + } + + $types = ($type = $eventArg->getType() instanceof \ReflectionUnionType ? $eventArg->getType()->getTypes() : [$eventArg->getType()]) ?: []; + + foreach ($types as $type) { + if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { + $tagAttributes['event'] = $type->getName(); + + $definition->addTag('kernel.event_listener', $tagAttributes); } - $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(Route::class, static function (ChildDefinition $definition, Route $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { - $definition->addTag('controller.service_arguments'); + $definition->addTag('controller.service_arguments')->addTag('routing.controller'); }); $container->registerAttributeForAutoconfiguration(AsRemoteEventConsumer::class, static function (ChildDefinition $definition, AsRemoteEventConsumer $attribute): void { $definition->addTag('remote_event.consumer', ['consumer' => $attribute->name]); @@ -871,6 +922,10 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont { $loader->load('form.php'); + if (!class_exists(EnumFormTypeGuesser::class)) { + $container->removeDefinition('form.type_guesser.enum_type'); + } + if (null === $config['form']['csrf_protection']['enabled']) { $this->writeConfigEnabled('form.csrf_protection', $config['csrf_protection']['enabled'], $config['form']['csrf_protection']); } @@ -895,7 +950,7 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont } } - private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void + private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride, ?array $allowedHttpMethodOverride): void { $options = $config; unset($options['enabled']); @@ -918,6 +973,14 @@ private function registerHttpCacheConfiguration(array $config, ContainerBuilder ->setFactory([Request::class, 'enableHttpMethodParameterOverride']) ); } + + if (null !== $allowedHttpMethodOverride) { + $container->getDefinition('http_cache') + ->addArgument((new Definition('void')) + ->setFactory([Request::class, 'setAllowedHttpMethodOverride']) + ->addArgument($allowedHttpMethodOverride) + ); + } } private function registerEsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void @@ -1085,6 +1148,11 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Global transition counter per workflow $transitionCounter = 0; foreach ($workflow['transitions'] as $transition) { + foreach (['from', 'to'] as $direction) { + foreach ($transition[$direction] as $k => $arc) { + $transition[$direction][$k] = new Definition(Arc::class, [$arc['place'], $arc['weight'] ?? 1]); + } + } if ('workflow' === $type) { $transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++); $container->register($transitionId, Workflow\Transition::class) @@ -1108,7 +1176,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ foreach ($transition['to'] as $to) { $transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++); $container->register($transitionId, Workflow\Transition::class) - ->setArguments([$transition['name'], $from, $to]); + ->setArguments([$transition['name'], [$from], [$to]]); $transitions[] = new Reference($transitionId); if (isset($transition['guard'])) { $eventName = \sprintf('workflow.%s.guard.%s', $name, $transition['name']); @@ -1184,8 +1252,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Store to container $container->setDefinition($workflowId, $workflowDefinition); $container->setDefinition($definitionDefinitionId, $definitionDefinition); - $container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type); - $container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name); + $container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type, $name); // Add workflow to Registry if ($workflow['supports']) { @@ -1299,6 +1366,14 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co $loader->load('routing.php'); + if (!class_exists(RoutingXmlFileLoader::class)) { + $container->removeDefinition('routing.loader.xml'); + } + + if (!class_exists(AttributeServicesLoader::class)) { + $container->removeDefinition('routing.loader.attribute.services'); + } + if ($config['utf8']) { $container->getDefinition('routing.loader')->replaceArgument(1, ['utf8' => true]); } @@ -1420,7 +1495,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co $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'); + $container->registerAliasForArgument('assets._package_'.$name, PackageInterface::class, $name.'.package', $name); } } @@ -1783,22 +1858,37 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $files = ['xml' => [], 'yml' => []]; $this->registerValidatorMapping($container, $config, $files); - if (!empty($files['xml'])) { + if ($files['xml']) { $validatorBuilder->addMethodCall('addXmlMappings', [$files['xml']]); } - if (!empty($files['yml'])) { + if ($files['yml']) { $validatorBuilder->addMethodCall('addYamlMappings', [$files['yml']]); } $definition = $container->findDefinition('validator.email'); $definition->replaceArgument(0, $config['email_validation_mode']); - if (\array_key_exists('enable_attributes', $config) && $config['enable_attributes']) { + // When attributes are disabled, it means from runtime-discovery only; autoconfiguration should still happen. + // And when runtime-discovery of attributes is enabled, we can skip compile-time autoconfiguration in debug mode. + if (class_exists(ValidatorAttributeMetadataPass::class) && (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug')) && trait_exists(ArgumentTrait::class)) { + // The $reflector argument hints at where the attribute could be used + $container->registerAttributeForAutoconfiguration(Constraint::class, function (ChildDefinition $definition, Constraint $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) { + $definition->addTag('validator.attribute_metadata') + ->addTag('container.excluded', ['source' => 'because it\'s a validator constraint extension']); + }); + } + + $container->registerAttributeForAutoconfiguration(ExtendsValidationFor::class, function (ChildDefinition $definition, ExtendsValidationFor $attribute) { + $definition->addTag('validator.attribute_metadata', ['for' => $attribute->class]) + ->addTag('container.excluded', ['source' => 'because it\'s a validator constraint extension']); + }); + + if ($config['enable_attributes'] ?? false) { $validatorBuilder->addMethodCall('enableAttributeMapping'); } - if (\array_key_exists('static_method', $config) && $config['static_method']) { + if ($config['static_method'] ?? false) { foreach ($config['static_method'] as $methodName) { $validatorBuilder->addMethodCall('addMethodMapping', [$methodName]); } @@ -1837,9 +1927,11 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co $files['yaml' === $extension ? 'yml' : $extension][] = $path; }; - 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'); + if (!ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) { + $container->removeDefinition('validator.form.attribute_metadata'); + } elseif (!($r = new \ReflectionClass(Form::class))->getAttributes(Traverse::class) || !class_exists(ValidatorAttributeMetadataPass::class)) { + // BC with symfony/form & symfony/validator < 7.4 + $fileRecorder('xml', \dirname($r->getFileName()).'/Resources/config/validation.xml'); } foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) { @@ -1967,7 +2059,7 @@ private function registerSecurityCsrfConfiguration(array $config, ContainerBuild return; } - if (!class_exists(\Symfony\Component\Security\Csrf\CsrfToken::class)) { + if (!class_exists(CsrfToken::class)) { throw new LogicException('CSRF support cannot be enabled as the Security CSRF component is not installed. Try running "composer require symfony/security-csrf".'); } if (!$config['stateless_token_ids'] && !$this->isInitializedConfigEnabled('session')) { @@ -2042,14 +2134,42 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $serializerLoaders = []; - if (isset($config['enable_attributes']) && $config['enable_attributes']) { - $attributeLoader = new Definition(AttributeLoader::class); - $serializerLoaders[] = $attributeLoader; + // When attributes are disabled, it means from runtime-discovery only; autoconfiguration should still happen. + // And when runtime-discovery of attributes is enabled, we can skip compile-time autoconfiguration in debug mode. + if (class_exists(SerializerAttributeMetadataPass::class) && (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug'))) { + // The $reflector argument hints at where the attribute could be used + $configurator = function (ChildDefinition $definition, object $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) { + $definition->addTag('serializer.attribute_metadata'); + }; + $container->registerAttributeForAutoconfiguration(SerializerMapping\Context::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\Groups::class, $configurator); + + $configurator = function (ChildDefinition $definition, object $attribute, \ReflectionMethod|\ReflectionProperty $reflector) { + $definition->addTag('serializer.attribute_metadata'); + }; + $container->registerAttributeForAutoconfiguration(SerializerMapping\Ignore::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\MaxDepth::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedName::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedPath::class, $configurator); + + $container->registerAttributeForAutoconfiguration(SerializerMapping\DiscriminatorMap::class, function (ChildDefinition $definition) { + $definition->addTag('serializer.attribute_metadata'); + }); + } + + if (($config['enable_attributes'] ?? false) || class_exists(SerializerAttributeMetadataPass::class)) { + $serializerLoaders[] = new Reference('serializer.mapping.attribute_loader'); + + $container->getDefinition('serializer.mapping.attribute_loader') + ->replaceArgument(0, $config['enable_attributes'] ?? false); + } else { + // BC with symfony/serializer < 7.4 + $container->removeDefinition('serializer.mapping.attribute_services_loader'); } $fileRecorder = function ($extension, $path) use (&$serializerLoaders) { - $definition = new Definition(\in_array($extension, ['yaml', 'yml']) ? YamlFileLoader::class : XmlFileLoader::class, [$path]); + $definition = new Definition(\in_array($extension, ['yaml', 'yml'], true) ? YamlFileLoader::class : XmlFileLoader::class, [$path]); $serializerLoaders[] = $definition; }; @@ -2082,7 +2202,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $chainLoader->replaceArgument(0, $serializerLoaders); $container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders); - if (isset($config['name_converter']) && $config['name_converter']) { + if ($config['name_converter'] ?? false) { $container->setParameter('.serializer.name_converter', $config['name_converter']); $container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($config['name_converter'])); } @@ -2104,6 +2224,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->getDefinition('serializer.normalizer.property')->setArgument(5, $defaultContext); $container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []); + + $container->registerAttributeForAutoconfiguration(ExtendsSerializationFor::class, function (ChildDefinition $definition, ExtendsSerializationFor $attribute) { + $definition->addTag('serializer.attribute_metadata', ['for' => $attribute->class]) + ->addTag('container.excluded', ['source' => 'because it\'s a serializer metadata extension']); + }); } private function registerJsonStreamerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void @@ -2162,7 +2287,7 @@ private function registerPropertyInfoConfiguration(array $config, ContainerBuild } } - private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void + private function registerTypeInfoConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void { if (!class_exists(Type::class)) { throw new LogicException('TypeInfo support cannot be enabled as the TypeInfo component is not installed. Try running "composer require symfony/type-info".'); @@ -2171,7 +2296,8 @@ private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpF $loader->load('type_info.php'); if (ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/type-info'])) { - $container->register('type_info.resolver.string', StringTypeResolver::class); + $container->register('type_info.resolver.string', StringTypeResolver::class) + ->setArguments([null, null, $config['aliases']]); $container->register('type_info.resolver.reflection_parameter.phpdoc_aware', PhpDocAwareReflectionTypeResolver::class) ->setArguments([new Reference('type_info.resolver.reflection_parameter'), new Reference('type_info.resolver.string'), new Reference('type_info.type_context_factory')]); @@ -2188,6 +2314,8 @@ private function registerTypeInfoConfiguration(ContainerBuilder $container, PhpF \ReflectionProperty::class => new Reference('type_info.resolver.reflection_property.phpdoc_aware'), \ReflectionFunctionAbstract::class => new Reference('type_info.resolver.reflection_return.phpdoc_aware'), ] + $resolversLocator->getValues()); + + $container->getDefinition('type_info.type_context_factory')->replaceArgument(1, $config['aliases']); } } @@ -2195,6 +2323,11 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont { $loader->load('lock.php'); + // BC layer Lock < 7.4 + if (!interface_exists(DenormalizerInterface::class) || !class_exists(LockKeyNormalizer::class)) { + $container->removeDefinition('serializer.normalizer.lock_key'); + } + foreach ($config['resources'] as $resourceName => $resourceStores) { if (0 === \count($resourceStores)) { continue; @@ -2203,10 +2336,6 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont // Generate stores $storeDefinitions = []; foreach ($resourceStores as $resourceStore) { - if (null === $resourceStore) { - $resourceStore = 'null'; - } - $usedEnvs = []; $storeDsn = $container->resolveEnvPlaceholders($resourceStore, null, $usedEnvs); if (!$usedEnvs && !str_contains($resourceStore, ':') && !\in_array($resourceStore, ['flock', 'semaphore', 'in-memory', 'null'], true)) { @@ -2242,7 +2371,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont $container->setAlias('lock.factory', new Alias('lock.'.$resourceName.'.factory', false)); $container->setAlias(LockFactory::class, new Alias('lock.factory', false)); } else { - $container->registerAliasForArgument('lock.'.$resourceName.'.factory', LockFactory::class, $resourceName.'.lock.factory'); + $container->registerAliasForArgument('lock.'.$resourceName.'.factory', LockFactory::class, $resourceName.'.lock.factory', $resourceName); } } } @@ -2277,7 +2406,7 @@ private function registerSemaphoreConfiguration(array $config, ContainerBuilder $container->setAlias('semaphore.factory', new Alias('semaphore.'.$resourceName.'.factory', false)); $container->setAlias(SemaphoreFactory::class, new Alias('semaphore.factory', false)); } else { - $container->registerAliasForArgument('semaphore.'.$resourceName.'.factory', SemaphoreFactory::class, $resourceName.'.semaphore.factory'); + $container->registerAliasForArgument('semaphore.'.$resourceName.'.factory', SemaphoreFactory::class, $resourceName.'.semaphore.factory', $resourceName); } } } @@ -2360,6 +2489,10 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ], ]; + if (class_exists(AddDefaultStampsMiddleware::class)) { + array_unshift($defaultMiddleware['before'], ['id' => 'add_default_stamps_middleware']); + } + if ($lockEnabled && class_exists(DeduplicateMiddleware::class) && class_exists(LockFactory::class)) { $defaultMiddleware['before'][] = ['id' => 'deduplicate_middleware']; } else { @@ -2683,6 +2816,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $loader->load('http_client.php'); $options = $config['default_options'] ?? []; + $cachingOptions = $options['caching'] ?? ['enabled' => false]; + unset($options['caching']); $rateLimiter = $options['rate_limiter'] ?? null; unset($options['rate_limiter']); $retryOptions = $options['retry_failed'] ?? ['enabled' => false]; @@ -2706,6 +2841,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->removeAlias(HttpClient::class); } + if ($this->readConfigEnabled('http_client.caching', $container, $cachingOptions)) { + $this->registerCachingHttpClient($cachingOptions, $options, 'http_client', $container); + } + if (null !== $rateLimiter) { $this->registerThrottlingHttpClient($rateLimiter, 'http_client', $container); } @@ -2731,6 +2870,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $scope = $scopeConfig['scope'] ?? null; unset($scopeConfig['scope']); + $cachingOptions = $scopeConfig['caching'] ?? ['enabled' => false]; + unset($scopeConfig['caching']); $rateLimiter = $scopeConfig['rate_limiter'] ?? null; unset($scopeConfig['rate_limiter']); $retryOptions = $scopeConfig['retry_failed'] ?? ['enabled' => false]; @@ -2754,6 +2895,10 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder ; } + if ($this->readConfigEnabled('http_client.scoped_clients.'.$name.'.caching', $container, $cachingOptions)) { + $this->registerCachingHttpClient($cachingOptions, $scopeConfig, $name, $container); + } + if (null !== $rateLimiter) { $this->registerThrottlingHttpClient($rateLimiter, $name, $container); } @@ -2795,6 +2940,24 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder } } + private function registerCachingHttpClient(array $options, array $defaultOptions, string $name, ContainerBuilder $container): void + { + if (!class_exists(ChunkCacheItemNotFoundException::class)) { + throw new LogicException('Caching cannot be enabled as version 7.3+ of the HttpClient component is required.'); + } + + $container + ->register($name.'.caching', CachingHttpClient::class) + ->setDecoratedService($name, null, 13) // between RetryableHttpClient (10) and ThrottlingHttpClient (15) + ->setArguments([ + new Reference($name.'.caching.inner'), + new Reference($options['cache_pool']), + $defaultOptions, + $options['shared'], + $options['max_ttl'], + ]); + } + private function registerThrottlingHttpClient(string $rateLimiter, string $name, ContainerBuilder $container): void { if (!class_exists(ThrottlingHttpClient::class)) { @@ -2880,6 +3043,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co MailerBridge\Mailomat\Transport\MailomatTransportFactory::class => 'mailer.transport_factory.mailomat', MailerBridge\MailPace\Transport\MailPaceTransportFactory::class => 'mailer.transport_factory.mailpace', MailerBridge\Mailchimp\Transport\MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', + MailerBridge\MicrosoftGraph\Transport\MicrosoftGraphTransportFactory::class => 'mailer.transport_factory.microsoftgraph', MailerBridge\Postal\Transport\PostalTransportFactory::class => 'mailer.transport_factory.postal', MailerBridge\Postmark\Transport\PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', MailerBridge\Mailtrap\Transport\MailtrapTransportFactory::class => 'mailer.transport_factory.mailtrap', @@ -2932,7 +3096,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $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'])) { + if (\in_array(strtolower($name), ['from', 'to', 'cc', 'bcc', 'reply-to'], true)) { $value = (array) $value; } $headers->addMethodCall('addHeader', [$name, $value]); @@ -3199,6 +3363,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $loader->load('notifier_webhook.php'); $webhookRequestParsers = [ + NotifierBridge\Lox24\Webhook\Lox24RequestParser::class => 'notifier.webhook.request_parser.lox24', NotifierBridge\Smsbox\Webhook\SmsboxRequestParser::class => 'notifier.webhook.request_parser.smsbox', NotifierBridge\Sweego\Webhook\SweegoRequestParser::class => 'notifier.webhook.request_parser.sweego', NotifierBridge\Twilio\Webhook\TwilioRequestParser::class => 'notifier.webhook.request_parser.twilio', @@ -3302,13 +3467,11 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde $factoryAlias = $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); if (interface_exists(RateLimiterFactoryInterface::class)) { - $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); - $factoryAlias->setDeprecated('symfony/framework-bundle', '7.3', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName())); - $internalAliasId = \sprintf('.%s $%s.limiter', RateLimiterFactory::class, $name); + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter', $name); - if ($container->hasAlias($internalAliasId)) { - $container->getAlias($internalAliasId)->setDeprecated('symfony/framework-bundle', '7.3', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName())); - } + $factoryAlias->setDeprecated('symfony/framework-bundle', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); + $container->getAlias(\sprintf('.%s $%s.limiter', RateLimiterFactory::class, $name)) + ->setDeprecated('symfony/framework-bundle', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); } } @@ -3333,7 +3496,7 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde ))) ; - $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter', $name); } } @@ -3442,11 +3605,17 @@ private function registerHtmlSanitizerConfiguration(array $config, ContainerBuil } } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getXsdValidationBasePath(): string|false { return \dirname(__DIR__).'/Resources/config/schema'; } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getNamespace(): string { return 'http://symfony.com/schema/dic/symfony'; diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 300fe22fb..3ad1dedc2 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -16,6 +16,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ErrorLoggerCompilerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PhpConfigReferenceDumpPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; @@ -61,9 +62,11 @@ use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass; use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass; use Symfony\Component\Routing\DependencyInjection\AddExpressionLanguageProvidersPass; +use Symfony\Component\Routing\DependencyInjection\RoutingControllerPass; use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass; use Symfony\Component\Runtime\SymfonyRuntime; use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass; +use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass as SerializerAttributeMetadataPass; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; use Symfony\Component\Translation\DependencyInjection\DataCollectorTranslatorPass; use Symfony\Component\Translation\DependencyInjection\LoggingTranslatorPass; @@ -74,6 +77,7 @@ use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass; +use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass; use Symfony\Component\VarExporter\Internal\Hydrator; use Symfony\Component\VarExporter\Internal\Registry; use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass; @@ -103,8 +107,7 @@ public function boot(): void $_ENV['DOCTRINE_DEPRECATIONS'] = $_SERVER['DOCTRINE_DEPRECATIONS'] ??= 'trigger'; if (class_exists(SymfonyRuntime::class)) { - $handler = set_error_handler('var_dump'); - restore_error_handler(); + $handler = get_error_handler(); } else { $handler = [ErrorHandler::register(null, false)]; } @@ -117,6 +120,10 @@ public function boot(): void Request::enableHttpMethodParameterOverride(); } + if ($this->container->hasParameter('kernel.allowed_http_method_override')) { + Request::setAllowedHttpMethodOverride($this->container->getParameter('kernel.allowed_http_method_override')); + } + if ($this->container->hasParameter('kernel.trust_x_sendfile_type_header') && $this->container->getParameter('kernel.trust_x_sendfile_type_header')) { BinaryFileResponse::trustXSendfileTypeHeader(); } @@ -142,11 +149,15 @@ public function build(ContainerBuilder $container): void ]); } - $container->addCompilerPass(new AssetsContextPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION); + if ($container->hasParameter('.kernel.config_dir') && $container->hasParameter('.kernel.bundles_definition')) { + $container->addCompilerPass(new PhpConfigReferenceDumpPass($container->getParameter('.kernel.config_dir').'/reference.php', $container->getParameter('.kernel.bundles_definition'))); + } + $container->addCompilerPass(new AssetsContextPass()); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new RoutingResolverPass()); + $this->addCompilerPassIfExists($container, RoutingControllerPass::class); $this->addCompilerPassIfExists($container, DataCollectorTranslatorPass::class); $container->addCompilerPass(new ProfilerPass()); // must be registered before removing private services as some might be listeners/subscribers @@ -154,6 +165,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass($registerListenersPass, PassConfig::TYPE_BEFORE_REMOVING); $this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class); $this->addCompilerPassIfExists($container, AddValidatorInitializersPass::class); + $this->addCompilerPassIfExists($container, AttributeMetadataPass::class); $this->addCompilerPassIfExists($container, AddConsoleCommandPass::class, PassConfig::TYPE_BEFORE_REMOVING); // must be registered before the AddConsoleCommandPass $container->addCompilerPass(new TranslationLintCommandPass(), PassConfig::TYPE_BEFORE_REMOVING, 10); @@ -167,6 +179,7 @@ public function build(ContainerBuilder $container): void $this->addCompilerPassIfExists($container, TranslationDumperPass::class); $container->addCompilerPass(new FragmentRendererPass()); $this->addCompilerPassIfExists($container, SerializerPass::class); + $this->addCompilerPassIfExists($container, SerializerAttributeMetadataPass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); $this->addCompilerPassIfExists($container, PropertyInfoConstructorPass::class); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); diff --git a/Kernel/MicroKernelTrait.php b/Kernel/MicroKernelTrait.php index 5d2ecf289..4814fe9c8 100644 --- a/Kernel/MicroKernelTrait.php +++ b/Kernel/MicroKernelTrait.php @@ -230,4 +230,27 @@ public function loadRoutes(LoaderInterface $loader): RouteCollection return $collection; } + + /** + * Returns the kernel parameters. + * + * @return array + */ + protected function getKernelParameters(): array + { + $parameters = parent::getKernelParameters(); + $bundlesPath = $this->getBundlesPath(); + $bundlesDefinition = !is_file($bundlesPath) ? [FrameworkBundle::class => ['all' => true]] : require $bundlesPath; + $knownEnvs = [$this->environment => true]; + + foreach ($bundlesDefinition as $envs) { + $knownEnvs += $envs; + } + unset($knownEnvs['all']); + $parameters['.container.known_envs'] = array_keys($knownEnvs); + $parameters['.kernel.config_dir'] = $this->getConfigDir(); + $parameters['.kernel.bundles_definition'] = $bundlesDefinition; + + return $parameters; + } } diff --git a/KernelBrowser.php b/KernelBrowser.php index add2508ff..dec0c698c 100644 --- a/KernelBrowser.php +++ b/KernelBrowser.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\HttpKernelBrowser; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile; @@ -63,6 +64,35 @@ public function getProfile(): HttpProfile|false|null return $this->getContainer()->get('profiler')->loadProfileFromResponse($this->response); } + public function getSession(): ?SessionInterface + { + $container = $this->getContainer(); + + if (!$container->has('session.factory')) { + return null; + } + + $session = $container->get('session.factory')->createSession(); + + $cookieJar = $this->getCookieJar(); + $cookie = $cookieJar->get($session->getName()); + + if ($cookie instanceof Cookie) { + $session->setId($cookie->getValue()); + } + + $session->start(); + + if (!$cookie instanceof Cookie) { + $domains = array_unique(array_map(fn (Cookie $cookie) => $cookie->getName() === $session->getName() ? $cookie->getDomain() : '', $cookieJar->all())) ?: ['']; + foreach ($domains as $domain) { + $cookieJar->set(new Cookie($session->getName(), $session->getId(), domain: $domain)); + } + } + + return $session; + } + /** * Enables the profiler for the very next request. * @@ -116,20 +146,13 @@ public function loginUser(object $user, string $firewallContext = 'main', array $container = $this->getContainer(); $container->get('security.untracked_token_storage')->setToken($token); - if (!$container->has('session.factory')) { + if (!$session = $this->getSession()) { return $this; } - $session = $container->get('session.factory')->createSession(); $session->set('_security_'.$firewallContext, serialize($token)); $session->save(); - $domains = array_unique(array_map(fn (Cookie $cookie) => $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; } @@ -205,25 +228,25 @@ protected function getScript(object $request): string $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; + $container = $kernel->getContainer(); + $container = $container->has('test.service_container') ? $container->get('test.service_container') : $container; + $container->get('profiler')->enable(); + EOF; } $code = <<boot(); -$profilerCode + \$kernel = unserialize($kernel); + \$kernel->boot(); + $profilerCode -\$request = unserialize($request); -EOF; + \$request = unserialize($request); + EOF; return $code.$this->getHandleScript(); } diff --git a/Resources/config/console.php b/Resources/config/console.php index 7ef10bb52..b2bae8c0b 100644 --- a/Resources/config/console.php +++ b/Resources/config/console.php @@ -37,7 +37,6 @@ use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand; -use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; @@ -45,6 +44,7 @@ use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand; use Symfony\Component\ErrorHandler\Command\ErrorDumpCommand; +use Symfony\Component\Form\Command\DebugCommand; use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; use Symfony\Component\Messenger\Command\DebugCommand as MessengerDebugCommand; use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; @@ -60,6 +60,7 @@ use Symfony\Component\Translation\Command\TranslationPushCommand; use Symfony\Component\Translation\Command\XliffLintCommand; use Symfony\Component\Validator\Command\DebugCommand as ValidatorDebugCommand; +use Symfony\Component\Workflow\Command\WorkflowDumpCommand; use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface; return static function (ContainerConfigurator $container) { @@ -327,7 +328,7 @@ ]) ->tag('console.command') - ->set('console.command.form_debug', \Symfony\Component\Form\Command\DebugCommand::class) + ->set('console.command.form_debug', DebugCommand::class) ->args([ service('form.registry'), [], // All form types namespaces are stored here by FormPass diff --git a/Resources/config/form.php b/Resources/config/form.php index 3c936a284..fe85f6c51 100644 --- a/Resources/config/form.php +++ b/Resources/config/form.php @@ -14,6 +14,7 @@ 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\EnumFormTypeGuesser; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ColorType; use Symfony\Component\Form\Extension\Core\Type\FileType; @@ -76,6 +77,9 @@ ->args([service('validator.mapping.class_metadata_factory')]) ->tag('form.type_guesser') + ->set('form.type_guesser.enum_type', EnumFormTypeGuesser::class) + ->tag('form.type_guesser') + ->alias('form.property_accessor', 'property_accessor') ->set('form.choice_list_factory.default', DefaultChoiceListFactory::class) diff --git a/Resources/config/http_client.php b/Resources/config/http_client.php index a562c2598..c963af604 100644 --- a/Resources/config/http_client.php +++ b/Resources/config/http_client.php @@ -15,6 +15,7 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\HttplugClient; use Symfony\Component\HttpClient\Messenger\PingWebhookMessageHandler; @@ -25,6 +26,14 @@ return static function (ContainerConfigurator $container) { $container->services() + ->set('cache.http_client.pool') + ->parent('cache.app') + ->tag('cache.pool') + + ->set('cache.http_client', TagAwareAdapter::class) + ->args([service('cache.http_client.pool')]) + ->tag('cache.taggable', ['pool' => 'cache.http_client.pool']) + ->set('http_client.transport', HttpClientInterface::class) ->factory([HttpClient::class, 'create']) ->args([ diff --git a/Resources/config/lock.php b/Resources/config/lock.php index 4e1463621..2dd64af77 100644 --- a/Resources/config/lock.php +++ b/Resources/config/lock.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\Serializer\LockKeyNormalizer; use Symfony\Component\Lock\Store\CombinedStore; use Symfony\Component\Lock\Strategy\ConsensusStrategy; @@ -26,5 +27,8 @@ ->args([abstract_arg('Store')]) ->call('setLogger', [service('logger')->ignoreOnInvalid()]) ->tag('monolog.logger', ['channel' => 'lock']) + + ->set('serializer.normalizer.lock_key', LockKeyNormalizer::class) + ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -880]) ; }; diff --git a/Resources/config/mailer_transports.php b/Resources/config/mailer_transports.php index 2c79b4d55..e88e95166 100644 --- a/Resources/config/mailer_transports.php +++ b/Resources/config/mailer_transports.php @@ -24,6 +24,7 @@ use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory; use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory; +use Symfony\Component\Mailer\Bridge\MicrosoftGraph\Transport\MicrosoftGraphTransportFactory; use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory; @@ -60,6 +61,7 @@ 'mailjet' => MailjetTransportFactory::class, 'mailomat' => MailomatTransportFactory::class, 'mailpace' => MailPaceTransportFactory::class, + 'microsoftgraph' => MicrosoftGraphTransportFactory::class, 'native' => NativeTransportFactory::class, 'null' => NullTransportFactory::class, 'postal' => PostalTransportFactory::class, diff --git a/Resources/config/messenger.php b/Resources/config/messenger.php index e02cd1ca3..611114272 100644 --- a/Resources/config/messenger.php +++ b/Resources/config/messenger.php @@ -26,6 +26,7 @@ use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; use Symfony\Component\Messenger\Handler\RedispatchMessageHandler; use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware; +use Symfony\Component\Messenger\Middleware\AddDefaultStampsMiddleware; use Symfony\Component\Messenger\Middleware\DeduplicateMiddleware; use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware; use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware; @@ -93,6 +94,8 @@ service('lock.factory'), ]) + ->set('messenger.middleware.add_default_stamps_middleware', AddDefaultStampsMiddleware::class) + ->set('messenger.middleware.add_bus_name_stamp_middleware', AddBusNameStampMiddleware::class) ->abstract() diff --git a/Resources/config/notifier_webhook.php b/Resources/config/notifier_webhook.php index 0b30c33e2..2ea59c512 100644 --- a/Resources/config/notifier_webhook.php +++ b/Resources/config/notifier_webhook.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Notifier\Bridge\Lox24\Webhook\Lox24RequestParser; use Symfony\Component\Notifier\Bridge\Smsbox\Webhook\SmsboxRequestParser; use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser; use Symfony\Component\Notifier\Bridge\Twilio\Webhook\TwilioRequestParser; @@ -18,6 +19,9 @@ return static function (ContainerConfigurator $container) { $container->services() + ->set('notifier.webhook.request_parser.lox24', Lox24RequestParser::class) + ->alias(Lox24RequestParser::class, 'notifier.webhook.request_parser.lox24') + ->set('notifier.webhook.request_parser.smsbox', SmsboxRequestParser::class) ->alias(SmsboxRequestParser::class, 'notifier.webhook.request_parser.smsbox') diff --git a/Resources/config/profiling.php b/Resources/config/profiling.php index a81c53a63..ba734bee2 100644 --- a/Resources/config/profiling.php +++ b/Resources/config/profiling.php @@ -39,6 +39,7 @@ param('profiler_listener.only_main_requests'), ]) ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => '?reset']) ->set('console_profiler_listener', ConsoleProfilerListener::class) ->args([ diff --git a/Resources/config/routing.php b/Resources/config/routing.php index 8cdbbf33a..7f8d50183 100644 --- a/Resources/config/routing.php +++ b/Resources/config/routing.php @@ -26,6 +26,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Loader\AttributeDirectoryLoader; use Symfony\Component\Routing\Loader\AttributeFileLoader; +use Symfony\Component\Routing\Loader\AttributeServicesLoader; use Symfony\Component\Routing\Loader\ContainerLoader; use Symfony\Component\Routing\Loader\DirectoryLoader; use Symfony\Component\Routing\Loader\GlobFileLoader; @@ -98,6 +99,12 @@ ]) ->tag('routing.loader', ['priority' => -10]) + ->set('routing.loader.attribute.services', AttributeServicesLoader::class) + ->args([ + abstract_arg('classes tagged with "routing.controller"'), + ]) + ->tag('routing.loader', ['priority' => -10]) + ->set('routing.loader.attribute.directory', AttributeDirectoryLoader::class) ->args([ service('file_locator'), @@ -164,10 +171,10 @@ param('request_listener.http_port'), param('request_listener.https_port'), ]) - ->call('setParameter', [ - '_functions', - service('router.expression_language_provider')->ignoreOnInvalid(), - ]) + ->call('setParameters', [[ + '_functions' => service('router.expression_language_provider')->ignoreOnInvalid(), + '_locale' => '%kernel.default_locale%', + ]]) ->alias(RequestContext::class, 'router.request_context') ->set('router.expression_language_provider', ExpressionLanguageProvider::class) diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index a25383ad7..d55d45767 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -384,7 +384,18 @@ - + + + + + + + + + + + + @@ -464,6 +475,7 @@ + @@ -506,8 +518,8 @@ - - + + @@ -515,6 +527,14 @@ + + + + + + + + @@ -718,6 +738,7 @@ + @@ -745,6 +766,7 @@ + @@ -778,6 +800,13 @@ + + + + + + + diff --git a/Resources/config/serializer.php b/Resources/config/serializer.php index e0a256bbe..6d9d354b4 100644 --- a/Resources/config/serializer.php +++ b/Resources/config/serializer.php @@ -28,6 +28,7 @@ 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\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; @@ -151,6 +152,9 @@ ->set('serializer.mapping.chain_loader', LoaderChain::class) ->args([[]]) + ->set('serializer.mapping.attribute_loader', AttributeLoader::class) + ->args([true, []]) + // Class Metadata Factory ->set('serializer.mapping.class_metadata_factory', ClassMetadataFactory::class) ->args([service('serializer.mapping.chain_loader')]) diff --git a/Resources/config/type_info.php b/Resources/config/type_info.php index 71e3646a1..0cf5dcbf5 100644 --- a/Resources/config/type_info.php +++ b/Resources/config/type_info.php @@ -23,7 +23,10 @@ $container->services() // type context ->set('type_info.type_context_factory', TypeContextFactory::class) - ->args([service('type_info.resolver.string')->nullOnInvalid()]) + ->args([ + service('type_info.resolver.string')->nullOnInvalid(), + [], + ]) // type resolvers ->set('type_info.resolver', TypeResolver::class) diff --git a/Resources/config/validator.php b/Resources/config/validator.php index 535b42edc..0ab0d86c0 100644 --- a/Resources/config/validator.php +++ b/Resources/config/validator.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Form\Form; use Symfony\Component\Validator\Constraints\EmailValidator; use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; use Symfony\Component\Validator\Constraints\ExpressionValidator; @@ -127,5 +128,9 @@ service('property_info'), ]) ->tag('validator.auto_mapper') + + ->set('validator.form.attribute_metadata', Form::class) + ->tag('container.excluded') + ->tag('validator.attribute_metadata') ; }; diff --git a/Resources/config/web.php b/Resources/config/web.php index a4e975dac..17a585d58 100644 --- a/Resources/config/web.php +++ b/Resources/config/web.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; @@ -31,6 +32,7 @@ use Symfony\Component\HttpKernel\EventListener\CacheAttributeListener; use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener; use Symfony\Component\HttpKernel\EventListener\ErrorListener; +use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener; use Symfony\Component\HttpKernel\EventListener\LocaleListener; use Symfony\Component\HttpKernel\EventListener\ResponseListener; use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; @@ -145,6 +147,18 @@ ->set('controller.cache_attribute_listener', CacheAttributeListener::class) ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => '?reset']) + + ->set('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class) + ->args([ + service('uri_signer'), + ]) + ->tag('kernel.event_subscriber') + + ->set('controller.helper', ControllerHelper::class) + ->tag('container.service_subscriber') + + ->alias(ControllerHelper::class, 'controller.helper') ; }; diff --git a/Resources/config/web_link.php b/Resources/config/web_link.php index 64345cc99..df55d1947 100644 --- a/Resources/config/web_link.php +++ b/Resources/config/web_link.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\HttpHeaderParser; use Symfony\Component\WebLink\HttpHeaderSerializer; return static function (ContainerConfigurator $container) { @@ -20,6 +21,9 @@ ->set('web_link.http_header_serializer', HttpHeaderSerializer::class) ->alias(HttpHeaderSerializer::class, 'web_link.http_header_serializer') + ->set('web_link.http_header_parser', HttpHeaderParser::class) + ->alias(HttpHeaderParser::class, 'web_link.http_header_parser') + ->set('web_link.add_link_header_listener', AddLinkHeaderListener::class) ->args([ service('web_link.http_header_serializer'), diff --git a/Routing/Router.php b/Routing/Router.php index 9efa07fae..f9e41273c 100644 --- a/Routing/Router.php +++ b/Routing/Router.php @@ -36,6 +36,9 @@ class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberInterface { private array $collectedParameters = []; + /** + * @var \Closure(string):mixed + */ private \Closure $paramFetcher; /** diff --git a/Secrets/SodiumVault.php b/Secrets/SodiumVault.php index 2a8e5dcc8..5abdfdc70 100644 --- a/Secrets/SodiumVault.php +++ b/Secrets/SodiumVault.php @@ -228,7 +228,7 @@ private function export(string $filename, string $data): void private function createSecretsDir(): void { - if ($this->secretsDir && !is_dir($this->secretsDir) && !@mkdir($this->secretsDir, 0777, true) && !is_dir($this->secretsDir)) { + if ($this->secretsDir && !is_dir($this->secretsDir) && !@mkdir($this->secretsDir, 0o777, true) && !is_dir($this->secretsDir)) { throw new \RuntimeException(\sprintf('Unable to create the secrets directory (%s).', $this->secretsDir)); } diff --git a/Test/BrowserKitAssertionsTrait.php b/Test/BrowserKitAssertionsTrait.php index 1b7437b77..6086e75ec 100644 --- a/Test/BrowserKitAssertionsTrait.php +++ b/Test/BrowserKitAssertionsTrait.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\Constraint\LogicalNot; use PHPUnit\Framework\ExpectationFailedException; use Symfony\Component\BrowserKit\AbstractBrowser; +use Symfony\Component\BrowserKit\History; use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -28,24 +29,31 @@ */ trait BrowserKitAssertionsTrait { - public static function assertResponseIsSuccessful(string $message = '', bool $verbose = true): void + private static bool $defaultVerboseMode = true; + + public static function setBrowserKitAssertionsAsVerbose(bool $verbose): void { - self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful($verbose), $message); + self::$defaultVerboseMode = $verbose; } - public static function assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true): void + public static function assertResponseIsSuccessful(string $message = '', ?bool $verbose = null): void { - self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful($verbose ?? self::$defaultVerboseMode), $message); } - public static function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void + public static function assertResponseStatusCodeSame(int $expectedCode, string $message = '', ?bool $verbose = null): void { - self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose ?? self::$defaultVerboseMode), $message); } - public static function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true): void + public static function assertResponseFormatSame(?string $expectedFormat, string $message = '', ?bool $verbose = null): void { - $constraint = new ResponseConstraint\ResponseIsRedirected($verbose); + self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat, $verbose ?? self::$defaultVerboseMode), $message); + } + + public static function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', ?bool $verbose = null): void + { + $constraint = new ResponseConstraint\ResponseIsRedirected($verbose ?? self::$defaultVerboseMode); if ($expectedLocation) { if (class_exists(ResponseConstraint\ResponseHeaderLocationSame::class)) { $locationConstraint = new ResponseConstraint\ResponseHeaderLocationSame(self::getRequest(), $expectedLocation); @@ -100,9 +108,9 @@ public static function assertResponseCookieValueSame(string $name, string $expec ), $message); } - public static function assertResponseIsUnprocessable(string $message = '', bool $verbose = true): void + public static function assertResponseIsUnprocessable(string $message = '', ?bool $verbose = null): void { - self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable($verbose), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseIsUnprocessable($verbose ?? self::$defaultVerboseMode), $message); } public static function assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = ''): void @@ -115,6 +123,38 @@ public static function assertBrowserNotHasCookie(string $name, string $path = '/ self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHasCookie($name, $path, $domain)), $message); } + public static function assertBrowserHistoryIsOnFirstPage(string $message = ''): void + { + if (!method_exists(History::class, 'isFirstPage')) { + throw new \LogicException('The `assertBrowserHistoryIsOnFirstPage` method requires symfony/browser-kit >= 7.4.'); + } + self::assertThatForClient(new BrowserKitConstraint\BrowserHistoryIsOnFirstPage(), $message); + } + + public static function assertBrowserHistoryIsNotOnFirstPage(string $message = ''): void + { + if (!method_exists(History::class, 'isFirstPage')) { + throw new \LogicException('The `assertBrowserHistoryIsNotOnFirstPage` method requires symfony/browser-kit >= 7.4.'); + } + self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHistoryIsOnFirstPage()), $message); + } + + public static function assertBrowserHistoryIsOnLastPage(string $message = ''): void + { + if (!method_exists(History::class, 'isLastPage')) { + throw new \LogicException('The `assertBrowserHistoryIsOnLastPage` method requires symfony/browser-kit >= 7.4.'); + } + self::assertThatForClient(new BrowserKitConstraint\BrowserHistoryIsOnLastPage(), $message); + } + + public static function assertBrowserHistoryIsNotOnLastPage(string $message = ''): void + { + if (!method_exists(History::class, 'isLastPage')) { + throw new \LogicException('The `assertBrowserHistoryIsNotOnLastPage` method requires symfony/browser-kit >= 7.4.'); + } + self::assertThatForClient(new LogicalNot(new BrowserKitConstraint\BrowserHistoryIsOnLastPage()), $message); + } + public static function assertBrowserCookieValueSame(string $name, string $expectedValue, bool $raw = false, string $path = '/', ?string $domain = null, string $message = ''): void { self::assertThatForClient(LogicalAnd::fromConstraints( diff --git a/Test/MailerAssertionsTrait.php b/Test/MailerAssertionsTrait.php index 2308c3e2f..07f4c99f5 100644 --- a/Test/MailerAssertionsTrait.php +++ b/Test/MailerAssertionsTrait.php @@ -90,6 +90,11 @@ public static function assertEmailAddressContains(RawMessage $email, string $hea self::assertThat($email, new MimeConstraint\EmailAddressContains($headerName, $expectedValue), $message); } + public static function assertEmailAddressNotContains(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailAddressContains($headerName, $expectedValue)), $message); + } + public static function assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = ''): void { self::assertThat($email, new MimeConstraint\EmailSubjectContains($expectedValue), $message); diff --git a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php index 994151807..4184cb56e 100644 --- a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php +++ b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; @@ -31,7 +33,9 @@ use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; -class ConfigBuilderCacheWarmerTest extends TestCase +#[IgnoreDeprecations] +#[Group('legacy')] +final class ConfigBuilderCacheWarmerTest extends TestCase { private string $varDir; @@ -231,11 +235,17 @@ public function load(array $configs, ContainerBuilder $container): void { } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getXsdValidationBasePath(): string|false { return false; } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getNamespace(): string { return 'http://www.example.com/schema/acme'; @@ -330,9 +340,6 @@ public function addConfiguration(NodeDefinition $node): void self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php'); self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/SecurityConfig.php'); - self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig.php'); - self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/FormLoginConfig.php'); - self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/TokenConfig.php'); } } @@ -412,9 +419,8 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode = $treeBuilder->getRootNode(); $firewallNodeBuilder = $rootNode - ->fixXmlConfig('firewall') ->children() - ->arrayNode('firewalls') + ->arrayNode('firewalls', 'firewall') ->isRequired() ->requiresAtLeastOneElement() ->useAttributeAsKey('name') diff --git a/Tests/CacheWarmer/SerializerCacheWarmerTest.php b/Tests/CacheWarmer/SerializerCacheWarmerTest.php index f17aad0e3..5c19d2a3f 100644 --- a/Tests/CacheWarmer/SerializerCacheWarmerTest.php +++ b/Tests/CacheWarmer/SerializerCacheWarmerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Cache\Adapter\NullAdapter; @@ -38,9 +39,7 @@ private function getArrayPool(string $file): PhpArrayAdapter return $this->arrayPool = new PhpArrayAdapter($file, new NullAdapter()); } - /** - * @dataProvider loaderProvider - */ + #[DataProvider('loaderProvider')] public function testWarmUp(array $loaders) { $file = sys_get_temp_dir().'/cache-serializer.php'; @@ -57,9 +56,7 @@ public function testWarmUp(array $loaders) $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author')->isHit()); } - /** - * @dataProvider loaderProvider - */ + #[DataProvider('loaderProvider')] public function testWarmUpAbsoluteFilePath(array $loaders) { $file = sys_get_temp_dir().'/0/cache-serializer.php'; @@ -79,9 +76,7 @@ public function testWarmUpAbsoluteFilePath(array $loaders) $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author')->isHit()); } - /** - * @dataProvider loaderProvider - */ + #[DataProvider('loaderProvider')] public function testWarmUpWithoutBuildDir(array $loaders) { $file = sys_get_temp_dir().'/cache-serializer.php'; diff --git a/Tests/Command/AboutCommand/AboutCommandTest.php b/Tests/Command/AboutCommand/AboutCommandTest.php index bcf3c7fe0..044d816e0 100644 --- a/Tests/Command/AboutCommand/AboutCommandTest.php +++ b/Tests/Command/AboutCommand/AboutCommandTest.php @@ -34,7 +34,7 @@ public function testAboutWithReadableFiles() $this->fs->mkdir($kernel->getProjectDir()); $this->fs->dumpFile($kernel->getCacheDir().'/readable_file', 'The file content.'); - $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0777); + $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0o777); $tester = $this->createCommandTester($kernel); $ret = $tester->execute([]); @@ -43,7 +43,7 @@ public function testAboutWithReadableFiles() $this->assertStringContainsString('Cache directory', $tester->getDisplay()); $this->assertStringContainsString('Log directory', $tester->getDisplay()); - $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0777); + $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0o777); try { $this->fs->remove($kernel->getProjectDir()); @@ -62,7 +62,7 @@ public function testAboutWithUnreadableFiles() } $this->fs->dumpFile($kernel->getCacheDir().'/unreadable_file', 'The file content.'); - $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0222); + $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0o222); $tester = $this->createCommandTester($kernel); $ret = $tester->execute([]); @@ -71,7 +71,7 @@ public function testAboutWithUnreadableFiles() $this->assertStringContainsString('Cache directory', $tester->getDisplay()); $this->assertStringContainsString('Log directory', $tester->getDisplay()); - $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0777); + $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0o777); try { $this->fs->remove($kernel->getProjectDir()); @@ -82,7 +82,7 @@ public function testAboutWithUnreadableFiles() private function createCommandTester(TestAppKernel $kernel): CommandTester { $application = new Application($kernel); - $application->add(new AboutCommand()); + $application->addCommand(new AboutCommand()); return new CommandTester($application->find('about')); } diff --git a/Tests/Command/CachePoolClearCommandTest.php b/Tests/Command/CachePoolClearCommandTest.php index 3a927f217..dcf788134 100644 --- a/Tests/Command/CachePoolClearCommandTest.php +++ b/Tests/Command/CachePoolClearCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use Psr\Cache\CacheItemPoolInterface; use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; @@ -30,13 +31,11 @@ protected function setUp(): void $this->cachePool = $this->createMock(CacheItemPoolInterface::class); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $application = new Application($this->getKernel()); - $application->add(new CachePoolClearCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); + $application->addCommand(new CachePoolClearCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); $tester = new CommandCompletionTester($application->get('cache:pool:clear')); $suggestions = $tester->complete($input); diff --git a/Tests/Command/CachePoolDeleteCommandTest.php b/Tests/Command/CachePoolDeleteCommandTest.php index 3db39e121..afd3ecdd7 100644 --- a/Tests/Command/CachePoolDeleteCommandTest.php +++ b/Tests/Command/CachePoolDeleteCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use Psr\Cache\CacheItemPoolInterface; use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand; @@ -84,13 +85,11 @@ public function testCommandDeleteFailed() $tester->execute(['pool' => 'foo', 'key' => 'bar']); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $application = new Application($this->getKernel()); - $application->add(new CachePoolDeleteCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); + $application->addCommand(new CachePoolDeleteCommand(new Psr6CacheClearer(['foo' => $this->cachePool]), ['foo'])); $tester = new CommandCompletionTester($application->get('cache:pool:delete')); $suggestions = $tester->complete($input); @@ -125,7 +124,7 @@ private function getKernel(): MockObject&KernelInterface private function getCommandTester(KernelInterface $kernel): CommandTester { $application = new Application($kernel); - $application->add(new CachePoolDeleteCommand(new Psr6CacheClearer(['foo' => $this->cachePool]))); + $application->addCommand(new CachePoolDeleteCommand(new Psr6CacheClearer(['foo' => $this->cachePool]))); return new CommandTester($application->find('cache:pool:delete')); } diff --git a/Tests/Command/CachePruneCommandTest.php b/Tests/Command/CachePruneCommandTest.php index a2d0ad7fe..18a3622f2 100644 --- a/Tests/Command/CachePruneCommandTest.php +++ b/Tests/Command/CachePruneCommandTest.php @@ -77,7 +77,7 @@ private function getPruneableInterfaceMock(): MockObject&PruneableInterface private function getCommandTester(KernelInterface $kernel, RewindableGenerator $generator): CommandTester { $application = new Application($kernel); - $application->add(new CachePoolPruneCommand($generator)); + $application->addCommand(new CachePoolPruneCommand($generator)); return new CommandTester($application->find('cache:pool:prune')); } diff --git a/Tests/Command/EventDispatcherDebugCommandTest.php b/Tests/Command/EventDispatcherDebugCommandTest.php index 359196e11..7dc1e0dc6 100644 --- a/Tests/Command/EventDispatcherDebugCommandTest.php +++ b/Tests/Command/EventDispatcherDebugCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\EventDispatcherDebugCommand; use Symfony\Component\Console\Tester\CommandCompletionTester; @@ -20,9 +21,7 @@ class EventDispatcherDebugCommandTest extends TestCase { - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $tester = $this->createCommandCompletionTester(); diff --git a/Tests/Command/RouterMatchCommandTest.php b/Tests/Command/RouterMatchCommandTest.php index b6b6771f9..97b1859be 100644 --- a/Tests/Command/RouterMatchCommandTest.php +++ b/Tests/Command/RouterMatchCommandTest.php @@ -46,8 +46,8 @@ public function testWithNotMatchPath() private function createCommandTester(): CommandTester { $application = new Application($this->getKernel()); - $application->add(new RouterMatchCommand($this->getRouter())); - $application->add(new RouterDebugCommand($this->getRouter())); + $application->addCommand(new RouterMatchCommand($this->getRouter())); + $application->addCommand(new RouterDebugCommand($this->getRouter())); return new CommandTester($application->find('router:match')); } diff --git a/Tests/Command/SecretsDecryptToLocalCommandTest.php b/Tests/Command/SecretsDecryptToLocalCommandTest.php index 8a1c05d69..fb5a66194 100644 --- a/Tests/Command/SecretsDecryptToLocalCommandTest.php +++ b/Tests/Command/SecretsDecryptToLocalCommandTest.php @@ -11,15 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsDecryptToLocalCommand; use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Filesystem\Filesystem; -/** - * @requires extension sodium - */ +#[RequiresPhpExtension('sodium')] class SecretsDecryptToLocalCommandTest extends TestCase { private string $mainDir; diff --git a/Tests/Command/SecretsEncryptFromLocalCommandTest.php b/Tests/Command/SecretsEncryptFromLocalCommandTest.php index bbdce737b..db095c279 100644 --- a/Tests/Command/SecretsEncryptFromLocalCommandTest.php +++ b/Tests/Command/SecretsEncryptFromLocalCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsEncryptFromLocalCommand; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; @@ -18,9 +19,7 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Filesystem\Filesystem; -/** - * @requires extension sodium - */ +#[RequiresPhpExtension('sodium')] class SecretsEncryptFromLocalCommandTest extends TestCase { private string $vaultDir; diff --git a/Tests/Command/SecretsGenerateKeysCommandTest.php b/Tests/Command/SecretsGenerateKeysCommandTest.php index 9574782bf..2940fcfc3 100644 --- a/Tests/Command/SecretsGenerateKeysCommandTest.php +++ b/Tests/Command/SecretsGenerateKeysCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeysCommand; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; @@ -18,9 +19,7 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Filesystem\Filesystem; -/** - * @requires extension sodium - */ +#[RequiresPhpExtension('sodium')] class SecretsGenerateKeysCommandTest extends TestCase { private string $secretsDir; diff --git a/Tests/Command/SecretsListCommandTest.php b/Tests/Command/SecretsListCommandTest.php index 12d3ab2e8..de09d8941 100644 --- a/Tests/Command/SecretsListCommandTest.php +++ b/Tests/Command/SecretsListCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; @@ -19,9 +20,7 @@ class SecretsListCommandTest extends TestCase { - /** - * @backupGlobals enabled - */ + #[BackupGlobals(true)] public function testExecute() { $vault = $this->createMock(AbstractVault::class); diff --git a/Tests/Command/SecretsRemoveCommandTest.php b/Tests/Command/SecretsRemoveCommandTest.php index 2c12b6128..d09fa3c01 100644 --- a/Tests/Command/SecretsRemoveCommandTest.php +++ b/Tests/Command/SecretsRemoveCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsRemoveCommand; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; @@ -18,9 +19,7 @@ class SecretsRemoveCommandTest extends TestCase { - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(bool $withLocalVault, array $input, array $expectedSuggestions) { $vault = $this->createMock(AbstractVault::class); diff --git a/Tests/Command/SecretsRevealCommandTest.php b/Tests/Command/SecretsRevealCommandTest.php index d77d303d5..37065d1c0 100644 --- a/Tests/Command/SecretsRevealCommandTest.php +++ b/Tests/Command/SecretsRevealCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsRevealCommand; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; @@ -59,9 +60,7 @@ public function testFailedDecrypt() $this->assertStringContainsString('The secret "secretKey" could not be decrypted.', trim($tester->getDisplay(true))); } - /** - * @backupGlobals enabled - */ + #[BackupGlobals(true)] public function testLocalVaultOverride() { $vault = $this->createMock(AbstractVault::class); @@ -78,9 +77,7 @@ public function testLocalVaultOverride() $this->assertEquals('newSecretValue', trim($tester->getDisplay(true))); } - /** - * @backupGlobals enabled - */ + #[BackupGlobals(true)] public function testOnlyLocalVaultContainsName() { $vault = $this->createMock(AbstractVault::class); diff --git a/Tests/Command/SecretsSetCommandTest.php b/Tests/Command/SecretsSetCommandTest.php index 678fb417c..57db9c529 100644 --- a/Tests/Command/SecretsSetCommandTest.php +++ b/Tests/Command/SecretsSetCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; @@ -18,9 +19,7 @@ class SecretsSetCommandTest extends TestCase { - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $vault = $this->createMock(AbstractVault::class); diff --git a/Tests/Command/TranslationDebugCommandTest.php b/Tests/Command/TranslationDebugCommandTest.php index c6c91a857..e0e49fd2e 100644 --- a/Tests/Command/TranslationDebugCommandTest.php +++ b/Tests/Command/TranslationDebugCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -223,7 +224,7 @@ function ($path, $catalogue) use ($loadedMessages) { $command = new TranslationDebugCommand($translator, $loader, $extractor, $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, $enabledLocales); $application = new Application($kernel); - $application->add($command); + $application->addCommand($command); return $application->find('debug:translation'); } @@ -240,9 +241,7 @@ private function getBundle($path) return $bundle; } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $extractedMessagesWithDomains = [ diff --git a/Tests/Command/TranslationExtractCommandCompletionTest.php b/Tests/Command/TranslationExtractCommandCompletionTest.php index 6d2f22d96..49874fe7c 100644 --- a/Tests/Command/TranslationExtractCommandCompletionTest.php +++ b/Tests/Command/TranslationExtractCommandCompletionTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -30,9 +31,7 @@ class TranslationExtractCommandCompletionTest extends TestCase private Filesystem $fs; private string $translationDir; - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $tester = $this->createCommandCompletionTester(['messages' => ['foo' => 'foo']]); @@ -132,7 +131,7 @@ function ($path, $catalogue) use ($loadedMessages) { $command = new TranslationExtractCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']); $application = new Application($kernel); - $application->add($command); + $application->addCommand($command); return new CommandCompletionTester($application->find('translation:extract')); } diff --git a/Tests/Command/TranslationExtractCommandTest.php b/Tests/Command/TranslationExtractCommandTest.php index c5e78de12..276af4476 100644 --- a/Tests/Command/TranslationExtractCommandTest.php +++ b/Tests/Command/TranslationExtractCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -154,12 +155,12 @@ public function testFilterDuplicateTransPaths() if (preg_match('/\.[a-z]+$/', $transPath)) { if (!realpath(\dirname($transPath))) { - mkdir(\dirname($transPath), 0777, true); + mkdir(\dirname($transPath), 0o777, true); } touch($transPath); } else { - mkdir($transPath, 0777, true); + mkdir($transPath, 0o777, true); } } @@ -177,9 +178,7 @@ public function testFilterDuplicateTransPaths() $this->assertEquals($expectedPaths, $filteredTransPaths); } - /** - * @dataProvider removeNoFillProvider - */ + #[DataProvider('removeNoFillProvider')] public function testRemoveNoFillTranslationsMethod($noFillCounter, $messages) { // Preparing mock @@ -304,7 +303,7 @@ function (MessageCatalogue $catalogue) use ($writerMessages) { $command = new TranslationExtractCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths); $application = new Application($kernel); - $application->add($command); + $application->addCommand($command); return new CommandTester($application->find('translation:extract')); } diff --git a/Tests/Command/WorkflowDumpCommandTest.php b/Tests/Command/WorkflowDumpCommandTest.php deleted file mode 100644 index 284e97623..000000000 --- a/Tests/Command/WorkflowDumpCommandTest.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\Command; - -use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; -use Symfony\Component\Console\Application; -use Symfony\Component\Console\Tester\CommandCompletionTester; -use Symfony\Component\DependencyInjection\ServiceLocator; - -class WorkflowDumpCommandTest extends TestCase -{ - /** - * @dataProvider provideCompletionSuggestions - */ - public function testComplete(array $input, array $expectedSuggestions) - { - $application = new Application(); - $application->add(new WorkflowDumpCommand(new ServiceLocator([]))); - - $tester = new CommandCompletionTester($application->find('workflow:dump')); - $suggestions = $tester->complete($input, 2); - $this->assertSame($expectedSuggestions, $suggestions); - } - - public static function provideCompletionSuggestions(): iterable - { - yield 'option --dump-format' => [['--dump-format', ''], ['puml', 'mermaid', 'dot']]; - } -} diff --git a/Tests/Command/XliffLintCommandTest.php b/Tests/Command/XliffLintCommandTest.php index d5495ada9..f10834c5b 100644 --- a/Tests/Command/XliffLintCommandTest.php +++ b/Tests/Command/XliffLintCommandTest.php @@ -35,10 +35,10 @@ public function testGetHelp() { $command = new XliffLintCommand(); $expected = <<php %command.full_name% @AcmeDemoBundle -EOF; + php %command.full_name% @AcmeDemoBundle + EOF; $this->assertStringContainsString($expected, $command->getHelp()); } @@ -59,7 +59,12 @@ private function createCommandTester($application = null): CommandTester { if (!$application) { $application = new BaseApplication(); - $application->add(new XliffLintCommand()); + $command = new XliffLintCommand(); + if (method_exists($application, 'addCommand')) { + $application->addCommand($command); + } else { + $application->add($command); + } } $command = $application->find('lint:xliff'); diff --git a/Tests/Command/YamlLintCommandTest.php b/Tests/Command/YamlLintCommandTest.php index ec2093119..28c55a5ba 100644 --- a/Tests/Command/YamlLintCommandTest.php +++ b/Tests/Command/YamlLintCommandTest.php @@ -73,10 +73,10 @@ public function testGetHelp() { $command = new YamlLintCommand(); $expected = <<php %command.full_name% @AcmeDemoBundle -EOF; + php %command.full_name% @AcmeDemoBundle + EOF; $this->assertStringContainsString($expected, $command->getHelp()); } @@ -107,7 +107,12 @@ private function createCommandTester($application = null): CommandTester { if (!$application) { $application = new BaseApplication(); - $application->add(new YamlLintCommand()); + $command = new YamlLintCommand(); + if (method_exists($application, 'addCommand')) { + $application->addCommand($command); + } else { + $application->add($command); + } } $command = $application->find('lint:yaml'); diff --git a/Tests/Console/ApplicationTest.php b/Tests/Console/ApplicationTest.php index 0b92a813c..7f712107c 100644 --- a/Tests/Console/ApplicationTest.php +++ b/Tests/Console/ApplicationTest.php @@ -119,7 +119,7 @@ public function testBundleCommandCanOverriddeAPreExistingCommandWithTheSameName( $application = new Application($kernel); $newCommand = new Command('example'); - $application->add($newCommand); + $application->addCommand($newCommand); $this->assertSame($newCommand, $application->get('example')); } diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index eb18fbcc7..a6cbd3085 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -11,6 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum; use Symfony\Component\Console\Input\ArrayInput; @@ -39,8 +42,8 @@ protected function tearDown(): void putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } - /** @dataProvider getDescribeRouteCollectionTestData */ - public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription) + #[DataProvider('getDescribeRouteCollectionTestData')] + public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $routes); } @@ -50,8 +53,8 @@ public static function getDescribeRouteCollectionTestData(): array return static::getDescriptionTestData(ObjectsProvider::getRouteCollections()); } - /** @dataProvider getDescribeRouteCollectionWithHttpMethodFilterTestData */ - public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription) + #[DataProvider('getDescribeRouteCollectionWithHttpMethodFilterTestData')] + public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $routes, ['method' => $httpMethod]); } @@ -65,8 +68,8 @@ public static function getDescribeRouteCollectionWithHttpMethodFilterTestData(): } } - /** @dataProvider getDescribeRouteTestData */ - public function testDescribeRoute(Route $route, $expectedDescription) + #[DataProvider('getDescribeRouteTestData')] + public function testDescribeRoute(Route $route, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $route); } @@ -76,8 +79,8 @@ public static function getDescribeRouteTestData(): array return static::getDescriptionTestData(ObjectsProvider::getRoutes()); } - /** @dataProvider getDescribeContainerParametersTestData */ - public function testDescribeContainerParameters(ParameterBag $parameters, $expectedDescription) + #[DataProvider('getDescribeContainerParametersTestData')] + public function testDescribeContainerParameters(ParameterBag $parameters, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $parameters); } @@ -87,8 +90,8 @@ public static function getDescribeContainerParametersTestData(): array return static::getDescriptionTestData(ObjectsProvider::getContainerParameters()); } - /** @dataProvider getDescribeContainerBuilderTestData */ - public function testDescribeContainerBuilder(ContainerBuilder $builder, $expectedDescription, array $options) + #[DataProvider('getDescribeContainerBuilderTestData')] + public function testDescribeContainerBuilder(ContainerBuilder $builder, $expectedDescription, array $options, $file) { $this->assertDescription($expectedDescription, $builder, $options); } @@ -98,10 +101,8 @@ public static function getDescribeContainerBuilderTestData(): array return static::getContainerBuilderDescriptionTestData(ObjectsProvider::getContainerBuilders()); } - /** - * @dataProvider getDescribeContainerExistingClassDefinitionTestData - */ - public function testDescribeContainerExistingClassDefinition(Definition $definition, $expectedDescription) + #[DataProvider('getDescribeContainerExistingClassDefinitionTestData')] + public function testDescribeContainerExistingClassDefinition(Definition $definition, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $definition); } @@ -111,8 +112,8 @@ public static function getDescribeContainerExistingClassDefinitionTestData(): ar return static::getDescriptionTestData(ObjectsProvider::getContainerDefinitionsWithExistingClasses()); } - /** @dataProvider getDescribeContainerDefinitionTestData */ - public function testDescribeContainerDefinition(Definition $definition, $expectedDescription) + #[DataProvider('getDescribeContainerDefinitionTestData')] + public function testDescribeContainerDefinition(Definition $definition, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $definition); } @@ -122,8 +123,8 @@ public static function getDescribeContainerDefinitionTestData(): array return static::getDescriptionTestData(ObjectsProvider::getContainerDefinitions()); } - /** @dataProvider getDescribeContainerDefinitionWithArgumentsShownTestData */ - public function testDescribeContainerDefinitionWithArgumentsShown(Definition $definition, $expectedDescription) + #[DataProvider('getDescribeContainerDefinitionWithArgumentsShownTestData')] + public function testDescribeContainerDefinitionWithArgumentsShown(Definition $definition, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $definition, []); } @@ -142,8 +143,8 @@ public static function getDescribeContainerDefinitionWithArgumentsShownTestData( return static::getDescriptionTestData($definitionsWithArgs); } - /** @dataProvider getDescribeContainerAliasTestData */ - public function testDescribeContainerAlias(Alias $alias, $expectedDescription) + #[DataProvider('getDescribeContainerAliasTestData')] + public function testDescribeContainerAlias(Alias $alias, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $alias); } @@ -153,8 +154,8 @@ public static function getDescribeContainerAliasTestData(): array return static::getDescriptionTestData(ObjectsProvider::getContainerAliases()); } - /** @dataProvider getDescribeContainerDefinitionWhichIsAnAliasTestData */ - public function testDescribeContainerDefinitionWhichIsAnAlias(Alias $alias, $expectedDescription, ContainerBuilder $builder, $options = []) + #[DataProvider('getDescribeContainerDefinitionWhichIsAnAliasTestData')] + public function testDescribeContainerDefinitionWhichIsAnAlias(Alias $alias, $expectedDescription, ContainerBuilder $builder, $options = [], $file = null) { $this->assertDescription($expectedDescription, $builder, $options); } @@ -185,13 +186,12 @@ public static function getDescribeContainerDefinitionWhichIsAnAliasTestData(): a } /** - * The legacy group must be kept as deprecations will always be raised. - * - * @group legacy - * - * @dataProvider getDescribeContainerParameterTestData + * The #[IgnoreDeprecation] attribute must be kept as deprecations will always be raised. */ - public function testDescribeContainerParameter($parameter, $expectedDescription, array $options) + #[IgnoreDeprecations] + #[Group('legacy')] + #[DataProvider('getDescribeContainerParameterTestData')] + public function testDescribeContainerParameter($parameter, $expectedDescription, array $options, $file) { $this->assertDescription($expectedDescription, $parameter, $options); } @@ -213,8 +213,8 @@ public static function getDescribeContainerParameterTestData(): array return $data; } - /** @dataProvider getDescribeEventDispatcherTestData */ - public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options) + #[DataProvider('getDescribeEventDispatcherTestData')] + public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options, $file) { $this->assertDescription($expectedDescription, $eventDispatcher, $options); } @@ -224,8 +224,8 @@ public static function getDescribeEventDispatcherTestData(): array return static::getEventDispatcherDescriptionTestData(ObjectsProvider::getEventDispatchers()); } - /** @dataProvider getDescribeCallableTestData */ - public function testDescribeCallable($callable, $expectedDescription) + #[DataProvider('getDescribeCallableTestData')] + public function testDescribeCallable($callable, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $callable); } @@ -235,12 +235,10 @@ public static function getDescribeCallableTestData(): array return static::getDescriptionTestData(ObjectsProvider::getCallables()); } - /** - * @group legacy - * - * @dataProvider getDescribeDeprecatedCallableTestData - */ - public function testDescribeDeprecatedCallable($callable, $expectedDescription) + #[IgnoreDeprecations] + #[Group('legacy')] + #[DataProvider('getDescribeDeprecatedCallableTestData')] + public function testDescribeDeprecatedCallable($callable, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $callable); } @@ -250,7 +248,7 @@ public static function getDescribeDeprecatedCallableTestData(): array return static::getDescriptionTestData(ObjectsProvider::getDeprecatedCallables()); } - /** @dataProvider getClassDescriptionTestData */ + #[DataProvider('getClassDescriptionTestData')] public function testGetClassDescription($object, $expectedDescription) { $this->assertEquals($expectedDescription, $this->getDescriptor()->getClassDescription($object)); @@ -266,10 +264,8 @@ public static function getClassDescriptionTestData(): array ]; } - /** - * @dataProvider getDeprecationsTestData - */ - public function testGetDeprecations(ContainerBuilder $builder, $expectedDescription) + #[DataProvider('getDeprecationsTestData')] + public function testGetDeprecations(ContainerBuilder $builder, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $builder, ['deprecations' => true]); } @@ -357,7 +353,7 @@ private static function getEventDispatcherDescriptionTestData(array $objects): a return $data; } - /** @dataProvider getDescribeContainerBuilderWithPriorityTagsTestData */ + #[DataProvider('getDescribeContainerBuilderWithPriorityTagsTestData')] public function testDescribeContainerBuilderWithPriorityTags(ContainerBuilder $builder, $expectedDescription, array $options) { $this->assertDescription($expectedDescription, $builder, $options); diff --git a/Tests/Console/Descriptor/ObjectsProvider.php b/Tests/Console/Descriptor/ObjectsProvider.php index 8eb1c4386..07c66e9c8 100644 --- a/Tests/Console/Descriptor/ObjectsProvider.php +++ b/Tests/Console/Descriptor/ObjectsProvider.php @@ -34,7 +34,32 @@ public static function getRouteCollections() $collection1->add($name, $route); } - return ['route_collection_1' => $collection1]; + $routesWithGenericHost = new RouteCollection(); + $routesWithGenericHost->add('some_route', new RouteStub( + '/some-route', + ['_controller' => 'Controller'], + [], + [], + null, + ['https'], + )); + + $routesWithGenericScheme = new RouteCollection(); + $routesWithGenericScheme->add('some_route_with_host', new RouteStub( + '/some-route', + ['_controller' => 'strpos'], + [], + [], + 'symfony.com', + [], + )); + + return [ + 'empty_route_collection' => new RouteCollection(), + 'route_collection_1' => $collection1, + 'route_with_generic_host' => $routesWithGenericHost, + 'route_with_generic_scheme' => $routesWithGenericScheme, + ]; } public static function getRouteCollectionsByHttpMethod(): array diff --git a/Tests/Console/Descriptor/TextDescriptorTest.php b/Tests/Console/Descriptor/TextDescriptorTest.php index 34e16f5e4..101ac68a0 100644 --- a/Tests/Console/Descriptor/TextDescriptorTest.php +++ b/Tests/Console/Descriptor/TextDescriptorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\Console\Descriptor\TextDescriptor; use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\Routing\Route; @@ -45,11 +46,11 @@ public static function getDescribeRouteWithControllerLinkTestData() return $getDescribeData; } - /** @dataProvider getDescribeRouteWithControllerLinkTestData */ - public function testDescribeRouteWithControllerLink(Route $route, $expectedDescription) + #[DataProvider('getDescribeRouteWithControllerLinkTestData')] + public function testDescribeRouteWithControllerLink(Route $route, $expectedDescription, $file) { static::$fileLinkFormatter = new FileLinkFormatter('myeditor://open?file=%f&line=%l'); - parent::testDescribeRoute($route, str_replace('[:file:]', __FILE__, $expectedDescription)); + parent::testDescribeRoute($route, str_replace('[:file:]', __FILE__, $expectedDescription), $file); } } diff --git a/Tests/Controller/AbstractControllerTest.php b/Tests/Controller/AbstractControllerTest.php index 2024cb8f7..f778aa4be 100644 --- a/Tests/Controller/AbstractControllerTest.php +++ b/Tests/Controller/AbstractControllerTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\DependencyInjection\Container; @@ -101,7 +103,7 @@ public function testMissingParameterBag() $controller->setContainer($container); $this->expectException(ServiceNotFoundException::class); - $this->expectExceptionMessage('TestAbstractController::getParameter()" method is missing a parameter bag'); + $this->expectExceptionMessage('::getParameter()" method is missing a parameter bag'); $controller->getParameter('foo'); } @@ -389,9 +391,7 @@ public function testdenyAccessUnlessGranted() } } - /** - * @dataProvider provideDenyAccessUnlessGrantedSetsAttributesAsArray - */ + #[DataProvider('provideDenyAccessUnlessGrantedSetsAttributesAsArray')] public function testdenyAccessUnlessGrantedSetsAttributesAsArray($attribute, $exceptionAttributes) { $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); @@ -526,9 +526,7 @@ public function testRedirectToRoute() $this->assertSame(302, $response->getStatusCode()); } - /** - * @runInSeparateProcess - */ + #[RunInSeparateProcess] public function testAddFlash() { $flashBag = new FlashBag(); diff --git a/Tests/Controller/ControllerHelperTest.php b/Tests/Controller/ControllerHelperTest.php new file mode 100644 index 000000000..cb35c4757 --- /dev/null +++ b/Tests/Controller/ControllerHelperTest.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\Controller; + +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper; + +class ControllerHelperTest extends AbstractControllerTest +{ + protected function createController() + { + return new class extends ControllerHelper { + public function __construct() + { + } + + public function setContainer(ContainerInterface $container) + { + parent::__construct($container); + } + }; + } + + public function testSync() + { + $r = new \ReflectionClass(ControllerHelper::class); + $m = $r->getMethod('getSubscribedServices'); + $helperSrc = file($r->getFileName()); + $helperSrc = implode('', \array_slice($helperSrc, $m->getStartLine() - 1, $r->getEndLine() - $m->getStartLine())); + + $r = new \ReflectionClass(AbstractController::class); + $m = $r->getMethod('getSubscribedServices'); + $abstractSrc = file($r->getFileName()); + $code = [ + implode('', \array_slice($abstractSrc, $m->getStartLine() - 1, $m->getEndLine() - $m->getStartLine() + 1)), + ]; + + foreach ($r->getMethods(\ReflectionMethod::IS_PROTECTED) as $m) { + if ($m->getDocComment()) { + $code[] = ' '.$m->getDocComment(); + } + $code[] = substr_replace(implode('', \array_slice($abstractSrc, $m->getStartLine() - 1, $m->getEndLine() - $m->getStartLine() + 1)), 'public', 4, 9); + } + foreach ($r->getMethods(\ReflectionMethod::IS_PRIVATE) as $m) { + if ($m->getDocComment()) { + $code[] = ' '.$m->getDocComment(); + } + $code[] = implode('', \array_slice($abstractSrc, $m->getStartLine() - 1, $m->getEndLine() - $m->getStartLine() + 1)); + } + $code = implode("\n", $code); + + $this->assertSame($code, $helperSrc, 'Methods from AbstractController are not properly synced in ControllerHelper'); + } +} diff --git a/Tests/Controller/RedirectControllerTest.php b/Tests/Controller/RedirectControllerTest.php index 161424e0e..55f3b33c3 100644 --- a/Tests/Controller/RedirectControllerTest.php +++ b/Tests/Controller/RedirectControllerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\HttpFoundation\ParameterBag; @@ -60,9 +61,7 @@ public function testEmptyRoute() } } - /** - * @dataProvider provider - */ + #[DataProvider('provider')] public function testRoute($permanent, $keepRequestMethod, $keepQueryParams, $ignoreAttributes, $expectedCode, $expectedAttributes) { $request = new Request(); @@ -255,9 +254,7 @@ public static function urlRedirectProvider(): array ]; } - /** - * @dataProvider urlRedirectProvider - */ + #[DataProvider('urlRedirectProvider')] public function testUrlRedirect($scheme, $httpPort, $httpsPort, $requestScheme, $requestPort, $expectedPort) { $host = 'www.example.com'; @@ -287,9 +284,7 @@ public static function pathQueryParamsProvider(): array ]; } - /** - * @dataProvider pathQueryParamsProvider - */ + #[DataProvider('pathQueryParamsProvider')] public function testPathQueryParams($expectedUrl, $path, $queryString) { $scheme = 'http'; diff --git a/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php new file mode 100644 index 000000000..7ea127909 --- /dev/null +++ b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.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\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PhpConfigReferenceDumpPass; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class PhpConfigReferenceDumpPassTest extends TestCase +{ + private string $tempDir; + + protected function setUp(): void + { + $this->tempDir = sys_get_temp_dir().'/sf_test_config_reference'; + mkdir($this->tempDir, 0o777, true); + } + + protected function tearDown(): void + { + if (is_dir($this->tempDir)) { + $fs = new Filesystem(); + $fs->remove($this->tempDir); + } + } + + public function testProcessWithConfigDir() + { + $container = new ContainerBuilder(); + $container->setParameter('.container.known_envs', ['test', 'dev']); + + $pass = new PhpConfigReferenceDumpPass($this->tempDir.'/reference.php', [ + TestBundle::class => ['all' => true], + ]); + $pass->process($container); + + $referenceFile = $this->tempDir.'/reference.php'; + $this->assertFileExists($referenceFile); + + $content = file_get_contents($referenceFile); + $this->assertStringContainsString('namespace Symfony\Component\DependencyInjection\Loader\Configurator;', $content); + $this->assertStringContainsString('final class App extends AppReference', $content); + $this->assertStringContainsString('public static function config(array $config): array', $content); + } + + public function testProcessIgnoresFileWriteErrors() + { + if ('\\' === \DIRECTORY_SEPARATOR) { + self::markTestSkipped('Cannot reliably make directory read-only on Windows.'); + } + + // Create a read-only directory to simulate write errors + $readOnlyDir = $this->tempDir.'/readonly'; + mkdir($readOnlyDir, 0o444, true); + + $container = new ContainerBuilder(); + $container->setParameter('.container.known_envs', ['dev', 'prod', 'test']); + + $pass = new PhpConfigReferenceDumpPass($readOnlyDir.'/reference.php', [ + TestBundle::class => ['all' => true], + ]); + + $pass->process($container); + $this->assertFileDoesNotExist($readOnlyDir.'/reference.php'); + } + + public function testProcessGeneratesExpectedReferenceFile() + { + $container = new ContainerBuilder(); + $container->setParameter('.container.known_envs', ['dev', 'prod', 'test']); + + $extension = new TestExtension(); + $container->registerExtension($extension); + + $pass = new PhpConfigReferenceDumpPass($this->tempDir.'/reference.php', [ + TestBundle::class => ['all' => true], + ]); + $pass->process($container); + + if ($_ENV['TEST_GENERATE_FIXTURES'] ?? false) { + copy($this->tempDir.'/reference.php', __DIR__.'/../../Fixtures/reference.php'); + self::markTestIncomplete('TEST_GENERATE_FIXTURES is set'); + } + + $this->assertFileEquals(__DIR__.'/../../Fixtures/reference.php', $this->tempDir.'/reference.php'); + } +} + +class TestBundle extends Bundle +{ + public function getContainerExtension(): ?ExtensionInterface + { + return new TestExtension(); + } +} + +class TestExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container): void + { + } + + public function getNamespace(): string + { + return 'test'; + } + + public function getXsdValidationBasePath(): string + { + return ''; + } + + public function getAlias(): string + { + return 'test'; + } + + public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface + { + return new TestConfiguration(); + } +} + +class TestConfiguration implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('test'); + $rootNode = $treeBuilder->getRootNode(); + + if ($rootNode instanceof ArrayNodeDefinition) { + $rootNode + ->children() + ->scalarNode('enabled')->defaultFalse()->end() + ->arrayNode('options') + ->children() + ->scalarNode('name')->end() + ->integerNode('count')->end() + ->end() + ->end() + ->end(); + } + + return $treeBuilder; + } +} diff --git a/Tests/DependencyInjection/Compiler/ProfilerPassTest.php b/Tests/DependencyInjection/Compiler/ProfilerPassTest.php index 5a2215009..12dfc085d 100644 --- a/Tests/DependencyInjection/Compiler/ProfilerPassTest.php +++ b/Tests/DependencyInjection/Compiler/ProfilerPassTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; use Symfony\Bundle\FrameworkBundle\DataCollector\TemplateAwareDataCollectorInterface; @@ -98,9 +99,7 @@ public static function getTemplate(): string }]; } - /** - * @dataProvider provideValidCollectorWithTemplateUsingAutoconfigure - */ + #[DataProvider('provideValidCollectorWithTemplateUsingAutoconfigure')] public function testValidCollectorWithTemplateUsingAutoconfigure(TemplateAwareDataCollectorInterface $dataCollector) { $container = new ContainerBuilder(); diff --git a/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php b/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php index fc69d5bd1..3a6824e4f 100644 --- a/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php +++ b/Tests/DependencyInjection/Compiler/TestServiceContainerRefPassesTest.php @@ -33,44 +33,44 @@ public function testProcess() $container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32); $container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING); - $container->register('Test\public_service') + $container->register('test.public_service', 'stdClass') ->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')) + ->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') + $container->register('test.soon_private_service', 'stdClass') ->setPublic(true) ->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.42']) ; - $container->register('Test\soon_private_service_decorated') + $container->register('test.soon_private_service_decorated', 'stdClass') ->setPublic(true) ->addTag('container.private', ['package' => 'foo/bar', 'version' => '1.42']) ; - $container->register('Test\soon_private_service_decorator') - ->setDecoratedService('Test\soon_private_service_decorated') - ->setArguments(['Test\soon_private_service_decorator.inner']); + $container->register('test.soon_private_service_decorator', 'stdClass') + ->setDecoratedService('test.soon_private_service_decorated') + ->setArguments(['test.soon_private_service_decorator.inner']); - $container->register('Test\private_used_shared_service'); - $container->register('Test\private_unused_shared_service'); - $container->register('Test\private_used_non_shared_service')->setShared(false); - $container->register('Test\private_unused_non_shared_service')->setShared(false); + $container->register('test.private_used_shared_service', 'stdClass'); + $container->register('test.private_unused_shared_service', 'stdClass'); + $container->register('test.private_used_non_shared_service', 'stdClass')->setShared(false); + $container->register('test.private_unused_non_shared_service', 'stdClass')->setShared(false); $container->compile(); $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')), - 'Test\soon_private_service' => new ServiceClosureArgument(new Reference('.container.private.Test\soon_private_service')), - 'Test\soon_private_service_decorator' => new ServiceClosureArgument(new Reference('.container.private.Test\soon_private_service_decorated')), - 'Test\soon_private_service_decorated' => new ServiceClosureArgument(new Reference('.container.private.Test\soon_private_service_decorated')), + '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')), + 'test.soon_private_service' => new ServiceClosureArgument(new Reference('.container.private.test.soon_private_service')), + 'test.soon_private_service_decorator' => new ServiceClosureArgument(new Reference('.container.private.test.soon_private_service_decorated')), + 'test.soon_private_service_decorated' => new ServiceClosureArgument(new Reference('.container.private.test.soon_private_service_decorated')), ]; $privateServices = $container->getDefinition('test.private_services_locator')->getArgument(0); unset($privateServices[\Symfony\Component\DependencyInjection\ContainerInterface::class], $privateServices[ContainerInterface::class]); $this->assertEquals($expected, $privateServices); - $this->assertFalse($container->getDefinition('Test\private_used_non_shared_service')->isShared()); + $this->assertFalse($container->getDefinition('test.private_used_non_shared_service')->isShared()); } } diff --git a/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php b/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php index 5bbd93722..35f1e52a6 100644 --- a/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php +++ b/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php @@ -19,6 +19,7 @@ public static function getDefinedTags(): array { $tags = [ 'proxy' => true, + 'routing.controller' => true, ]; // get all tags used in XML configs diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 7abc59bcb..d2ec77164 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -12,8 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\DBAL\Connection; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; +use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places; use Symfony\Bundle\FullStack; use Symfony\Component\AssetMapper\Compressor\CompressorInterface; use Symfony\Component\Cache\Adapter\DoctrineAdapter; @@ -61,9 +64,7 @@ public function getTestValidSessionName() ]; } - /** - * @dataProvider getTestInvalidSessionName - */ + #[DataProvider('getTestInvalidSessionName')] public function testInvalidSessionName($sessionName) { $processor = new Processor(); @@ -153,9 +154,7 @@ public function testAssetMapperCanBeEnabled() $this->assertEquals($defaultConfig, $config['asset_mapper']); } - /** - * @dataProvider provideImportmapPolyfillTests - */ + #[DataProvider('provideImportmapPolyfillTests')] public function testAssetMapperPolyfillValue(mixed $polyfillValue, bool $isValid, mixed $expected) { $processor = new Processor(); @@ -189,9 +188,7 @@ public static function provideImportmapPolyfillTests() yield [false, true, false]; } - /** - * @dataProvider provideValidAssetsPackageNameConfigurationTests - */ + #[DataProvider('provideValidAssetsPackageNameConfigurationTests')] public function testValidAssetsPackageNameConfiguration($packageName) { $processor = new Processor(); @@ -221,9 +218,7 @@ public static function provideValidAssetsPackageNameConfigurationTests(): array ]; } - /** - * @dataProvider provideInvalidAssetConfigurationTests - */ + #[DataProvider('provideInvalidAssetConfigurationTests')] public function testInvalidAssetsConfiguration(array $assetConfig, $expectedMessage) { $processor = new Processor(); @@ -275,9 +270,7 @@ public static function provideInvalidAssetConfigurationTests(): iterable yield [$createPackageConfig($config), 'You cannot use both "version" and "json_manifest_path" at the same time under "assets" packages.']; } - /** - * @dataProvider provideValidLockConfigurationTests - */ + #[DataProvider('provideValidLockConfigurationTests')] public function testValidLockConfiguration($lockConfig, $processedConfig) { $processor = new Processor(); @@ -375,9 +368,7 @@ public function testLockMergeConfigs() ); } - /** - * @dataProvider provideValidSemaphoreConfigurationTests - */ + #[DataProvider('provideValidSemaphoreConfigurationTests')] public function testValidSemaphoreConfiguration($semaphoreConfig, $processedConfig) { $processor = new Processor(); @@ -706,6 +697,48 @@ public function testSerializerJsonDetailedErrorMessagesNotSetByDefaultWithDebugD $this->assertSame([], $config['serializer']['default_context'] ?? []); } + public function testWorkflowEnumArcsNormalization() + { + $processor = new Processor(); + $configuration = new Configuration(true); + + $config = $processor->processConfiguration($configuration, [[ + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'workflows' => [ + 'workflows' => [ + 'enum' => [ + 'supports' => [self::class], + 'places' => Places::cases(), + 'transitions' => [ + [ + 'name' => 'one', + 'from' => [Places::A], + 'to' => [['place' => Places::B, 'weight' => 2]], + ], + [ + 'name' => 'two', + 'from' => ['place' => Places::B, 'weight' => 3], + 'to' => ['place' => Places::C], + ], + ], + ], + ], + ], + ]]); + + $transitions = $config['workflows']['workflows']['enum']['transitions']; + + $this->assertSame('one', $transitions[0]['name']); + $this->assertSame([['place' => 'a', 'weight' => 1]], $transitions[0]['from']); + $this->assertSame([['place' => 'b', 'weight' => 2]], $transitions[0]['to']); + + $this->assertSame('two', $transitions[1]['name']); + $this->assertSame([['place' => 'b', 'weight' => 3]], $transitions[1]['from']); + $this->assertSame([['place' => 'c', 'weight' => 1]], $transitions[1]['to']); + } + public function testFormCsrfProtectionFieldAttrDoNotNormalizeKeys() { $processor = new Processor(); @@ -722,10 +755,30 @@ public function testFormCsrfProtectionFieldAttrDoNotNormalizeKeys() $this->assertSame(['data-example-attr' => 'value'], $config['form']['csrf_protection']['field_attr'] ?? []); } + #[TestWith(['CONNECT'])] + #[TestWith(['GET'])] + #[TestWith(['HEAD'])] + #[TestWith(['TRACE'])] + public function testInvalidHttpMethodOverride(string $method) + { + $processor = new Processor(); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The HTTP methods "GET", "HEAD", "CONNECT", and "TRACE" cannot be overridden.'); + + $processor->processConfiguration( + new Configuration(true), + [[ + 'allowed_http_method_override' => [$method], + ]] + ); + } + protected static function getBundleDefaultConfig() { return [ 'http_method_override' => false, + 'allowed_http_method_override' => null, 'handle_all_throwables' => true, 'trust_x_sendfile_type_header' => '%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%', 'ide' => '%env(default::SYMFONY_IDE)%', @@ -823,6 +876,7 @@ protected static function getBundleDefaultConfig() ], 'type_info' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(Type::class), + 'aliases' => [], ], 'property_info' => [ 'enabled' => !class_exists(FullStack::class), diff --git a/Tests/DependencyInjection/Fixtures/Workflow/Places.php b/Tests/DependencyInjection/Fixtures/Workflow/Places.php new file mode 100644 index 000000000..47a6b5a4c --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/Workflow/Places.php @@ -0,0 +1,10 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'http_client' => [ + 'default_options' => [ + 'headers' => ['X-powered' => 'PHP'], + 'caching' => [ + 'cache_pool' => 'foo', + 'shared' => false, + 'max_ttl' => 2, + ], + ], + 'scoped_clients' => [ + 'bar' => [ + 'base_uri' => 'http://example.com', + 'caching' => ['cache_pool' => 'baz'], + ], + ], + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php b/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php index 3e203028c..2ae08c77a 100644 --- a/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php +++ b/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php @@ -6,7 +6,7 @@ 'handle_all_throwables' => true, 'php_errors' => ['log' => true], 'serializer' => [ - 'enable_attributes' => false, + 'enable_attributes' => true, 'mapping' => [ 'paths' => [ '%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files', diff --git a/Tests/DependencyInjection/Fixtures/php/type_info.php b/Tests/DependencyInjection/Fixtures/php/type_info.php index 0e7dcbae0..3bf8b3150 100644 --- a/Tests/DependencyInjection/Fixtures/php/type_info.php +++ b/Tests/DependencyInjection/Fixtures/php/type_info.php @@ -7,5 +7,8 @@ 'php_errors' => ['log' => true], 'type_info' => [ 'enabled' => true, + 'aliases' => [ + 'CustomAlias' => 'int', + ], ], ]); diff --git a/Tests/DependencyInjection/Fixtures/php/workflow_enum_places.php b/Tests/DependencyInjection/Fixtures/php/workflow_enum_places.php new file mode 100644 index 000000000..cc45f350a --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/workflow_enum_places.php @@ -0,0 +1,25 @@ +loadFromExtension('framework', [ + 'workflows' => [ + 'enum' => [ + 'supports' => [ + FrameworkExtensionTestCase::class, + ], + 'places' => Places::cases(), + 'transitions' => [ + 'one' => [ + 'from' => Places::A, + 'to' => Places::B, + ], + 'two' => [ + 'from' => Places::B, + 'to' => Places::C, + ], + ], + ] + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/workflow_glob_places.php b/Tests/DependencyInjection/Fixtures/php/workflow_glob_places.php new file mode 100644 index 000000000..092e4a183 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/workflow_glob_places.php @@ -0,0 +1,25 @@ +loadFromExtension('framework', [ + 'workflows' => [ + 'enum' => [ + 'supports' => [ + FrameworkExtensionTestCase::class, + ], + 'places' => Places::class.'::*', + 'transitions' => [ + 'one' => [ + 'from' => Places::A, + 'to' => Places::B, + ], + 'two' => [ + 'from' => Places::B, + 'to' => Places::C, + ], + ], + ] + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php b/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php index f4956eccb..93a415fd8 100644 --- a/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php +++ b/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php @@ -22,13 +22,14 @@ 'approved_by_spellchecker', 'published', ], + // We also test different configuration formats here 'transitions' => [ 'request_review' => [ 'from' => 'draft', 'to' => ['wait_for_journalist', 'wait_for_spellchecker'], ], 'journalist_approval' => [ - 'from' => 'wait_for_journalist', + 'from' => ['wait_for_journalist'], 'to' => 'approved_by_journalist', ], 'spellchecker_approval' => [ @@ -36,13 +37,13 @@ 'to' => 'approved_by_spellchecker', ], 'publish' => [ - 'from' => ['approved_by_journalist', 'approved_by_spellchecker'], + 'from' => [['place' => 'approved_by_journalist', 'weight' => 1], 'approved_by_spellchecker'], 'to' => 'published', ], 'publish_editor_in_chief' => [ 'name' => 'publish', 'from' => 'draft', - 'to' => 'published', + 'to' => [['place' => 'published', 'weight' => 2]], ], ], ], diff --git a/Tests/DependencyInjection/Fixtures/xml/http_client_caching.xml b/Tests/DependencyInjection/Fixtures/xml/http_client_caching.xml new file mode 100644 index 000000000..d7a51ffc5 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/http_client_caching.xml @@ -0,0 +1,21 @@ + + + + + + + + + PHP + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml b/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml index bb8dccf9c..165669fe6 100644 --- a/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml +++ b/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml @@ -7,7 +7,7 @@ - + %kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files %kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/serialization.yml diff --git a/Tests/DependencyInjection/Fixtures/xml/type_info.xml b/Tests/DependencyInjection/Fixtures/xml/type_info.xml index 0fe4d525d..b3ca5e08a 100644 --- a/Tests/DependencyInjection/Fixtures/xml/type_info.xml +++ b/Tests/DependencyInjection/Fixtures/xml/type_info.xml @@ -8,6 +8,8 @@ - + + int + diff --git a/Tests/DependencyInjection/Fixtures/xml/workflow_glob_places.xml b/Tests/DependencyInjection/Fixtures/xml/workflow_glob_places.xml new file mode 100644 index 000000000..f5d198b5b --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/workflow_glob_places.xml @@ -0,0 +1,23 @@ + + + + + + + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase + + a + b + + + b + c + + + + diff --git a/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml b/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml index 0435447b6..b7f2724a5 100644 --- a/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml +++ b/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml @@ -18,6 +18,7 @@ + draft wait_for_journalist @@ -32,13 +33,13 @@ approved_by_spellchecker - approved_by_journalist - approved_by_spellchecker + approved_by_journalist + approved_by_spellchecker published draft - published + published diff --git a/Tests/DependencyInjection/Fixtures/yml/http_client_caching.yml b/Tests/DependencyInjection/Fixtures/yml/http_client_caching.yml new file mode 100644 index 000000000..1c7012810 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/http_client_caching.yml @@ -0,0 +1,19 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + http_client: + default_options: + headers: + X-powered: PHP + caching: + cache_pool: foo + shared: false + max_ttl: 2 + scoped_clients: + bar: + base_uri: http://example.com + caching: + cache_pool: baz diff --git a/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml b/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml index 46425dc94..3c0e8be3b 100644 --- a/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml +++ b/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml @@ -5,7 +5,7 @@ framework: php_errors: log: true serializer: - enable_attributes: false + enable_attributes: true mapping: paths: - "%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files" diff --git a/Tests/DependencyInjection/Fixtures/yml/type_info.yml b/Tests/DependencyInjection/Fixtures/yml/type_info.yml index 4d6b405b2..bca245dfd 100644 --- a/Tests/DependencyInjection/Fixtures/yml/type_info.yml +++ b/Tests/DependencyInjection/Fixtures/yml/type_info.yml @@ -6,3 +6,5 @@ framework: log: true type_info: enabled: true + aliases: + CustomAlias: int diff --git a/Tests/DependencyInjection/Fixtures/yml/workflow_enum_places.yml b/Tests/DependencyInjection/Fixtures/yml/workflow_enum_places.yml new file mode 100644 index 000000000..d72eab0e3 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/workflow_enum_places.yml @@ -0,0 +1,13 @@ +framework: + workflows: + enum: + supports: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase + places: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places + transitions: + one: + from: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::A + to: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::B + two: + from: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::B + to: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::C diff --git a/Tests/DependencyInjection/Fixtures/yml/workflow_glob_places.yml b/Tests/DependencyInjection/Fixtures/yml/workflow_glob_places.yml new file mode 100644 index 000000000..b0591682f --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/workflow_glob_places.yml @@ -0,0 +1,13 @@ +framework: + workflows: + enum: + supports: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase + places: 'Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::*' + transitions: + one: + from: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::A + to: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::B + two: + from: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::B + to: !php/enum Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Places::C diff --git a/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml b/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml index 67eccb425..a3f52e05d 100644 --- a/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml +++ b/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml @@ -17,20 +17,21 @@ framework: - wait_for_spellchecker - approved_by_spellchecker - published + # We also test different configuration formats here transitions: request_review: - from: [draft] + from: draft to: [wait_for_journalist, wait_for_spellchecker] journalist_approval: from: [wait_for_journalist] - to: [approved_by_journalist] + to: approved_by_journalist spellchecker_approval: - from: [wait_for_spellchecker] - to: [approved_by_spellchecker] + from: wait_for_spellchecker + to: approved_by_spellchecker publish: - from: [approved_by_journalist, approved_by_spellchecker] - to: [published] + from: [{place: approved_by_journalist, weight: 1}, approved_by_spellchecker] + to: published publish_editor_in_chief: name: publish - from: [draft] - to: [published] + from: draft + to: [{place: published, weight: 2}] diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 5cd73fcf4..5b0f1263d 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -11,8 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; +use Psr\Log\LogLevel; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator; @@ -52,12 +54,18 @@ use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface; +use Symfony\Component\HttpClient\CachingHttpClient; +use Symfony\Component\HttpClient\Exception\ChunkCacheItemNotFoundException; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpClient\ThrottlingHttpClient; use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory; @@ -71,7 +79,6 @@ use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; -use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; @@ -87,11 +94,13 @@ use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Translation\LocaleSwitcher; use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Component\Validator\Constraints\Traverse; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Webhook\Client\RequestParser; use Symfony\Component\Webhook\Controller\WebhookController; +use Symfony\Component\Workflow\Arc; use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; @@ -205,6 +214,30 @@ public function testHttpMethodOverride() $this->assertFalse($container->getParameter('kernel.http_method_override')); } + public function testAllowedHttpMethodOverride() + { + $container = $this->createContainerFromFile('full'); + + $this->assertNull($container->getParameter('kernel.allowed_http_method_override')); + } + + public function testAllowedHttpMethodOverrideWithSpecificMethods() + { + $container = $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => true, + 'allowed_http_method_override' => ['PUT', 'DELETE'], + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'secret' => 's3cr3t', + ]); + }); + + $this->assertTrue($container->getParameter('kernel.http_method_override')); + $this->assertEquals(['PUT', 'DELETE'], $container->getParameter('kernel.allowed_http_method_override')); + } + public function testTrustXSendfileTypeHeader() { $container = $this->createContainerFromFile('full'); @@ -456,61 +489,137 @@ public function testWorkflowMultipleTransitionsWithSameName() $this->assertCount(5, $transitions); - $this->assertSame('.workflow.article.transition.0', (string) $transitions[0]); - $this->assertSame([ + $this->assertTransitionReference( + $container, + '.workflow.article.transition.0', 'request_review', [ - 'draft', + ['place' => 'draft', 'weight' => 1], ], [ - 'wait_for_journalist', 'wait_for_spellchecker', + ['place' => 'wait_for_journalist', 'weight' => 1], + ['place' => 'wait_for_spellchecker', 'weight' => 1], ], - ], $container->getDefinition($transitions[0])->getArguments()); + $transitions[0] + ); - $this->assertSame('.workflow.article.transition.1', (string) $transitions[1]); - $this->assertSame([ + $this->assertTransitionReference( + $container, + '.workflow.article.transition.1', 'journalist_approval', [ - 'wait_for_journalist', + ['place' => 'wait_for_journalist', 'weight' => 1], ], [ - 'approved_by_journalist', + ['place' => 'approved_by_journalist', 'weight' => 1], ], - ], $container->getDefinition($transitions[1])->getArguments()); + $transitions[1] + ); - $this->assertSame('.workflow.article.transition.2', (string) $transitions[2]); - $this->assertSame([ + $this->assertTransitionReference( + $container, + '.workflow.article.transition.2', 'spellchecker_approval', [ - 'wait_for_spellchecker', + ['place' => 'wait_for_spellchecker', 'weight' => 1], ], [ - 'approved_by_spellchecker', + ['place' => 'approved_by_spellchecker', 'weight' => 1], ], - ], $container->getDefinition($transitions[2])->getArguments()); + $transitions[2] + ); - $this->assertSame('.workflow.article.transition.3', (string) $transitions[3]); - $this->assertSame([ + $this->assertTransitionReference( + $container, + '.workflow.article.transition.3', 'publish', [ - 'approved_by_journalist', - 'approved_by_spellchecker', + ['place' => 'approved_by_journalist', 'weight' => 1], + ['place' => 'approved_by_spellchecker', 'weight' => 1], ], [ - 'published', + ['place' => 'published', 'weight' => 1], ], - ], $container->getDefinition($transitions[3])->getArguments()); + $transitions[3] + ); - $this->assertSame('.workflow.article.transition.4', (string) $transitions[4]); - $this->assertSame([ + $this->assertTransitionReference( + $container, + '.workflow.article.transition.4', 'publish', [ - 'draft', + ['place' => 'draft', 'weight' => 1], ], [ - 'published', + ['place' => 'published', 'weight' => 2], ], - ], $container->getDefinition($transitions[4])->getArguments()); + $transitions[4] + ); + } + + private function assertTransitionReference(ContainerBuilder $container, string $expectedServiceId, string $expectedName, array $expectedFroms, array $expectedTos, Reference $transition): void + { + $this->assertSame($expectedServiceId, (string) $transition); + + $args = $container->getDefinition($transition)->getArguments(); + $this->assertTransition($expectedName, $expectedFroms, $expectedTos, $args); + } + + private function assertTransition(string $expectedName, array $expectedFroms, array $expectedTos, array $args): void + { + $this->assertCount(3, $args); + $this->assertSame($expectedName, $args[0]); + + $this->assertCount(\count($expectedFroms), $args[1]); + foreach ($expectedFroms as $i => ['place' => $place, 'weight' => $weight]) { + $this->assertInstanceOf(Definition::class, $args[1][$i]); + $this->assertSame(Arc::class, $args[1][$i]->getClass()); + $arcArgs = array_values($args[1][$i]->getArguments()); + $this->assertSame($place, $arcArgs[0]); + $this->assertSame($weight, $arcArgs[1]); + } + + $this->assertCount(\count($expectedTos), $args[2]); + foreach ($expectedTos as $i => ['place' => $place, 'weight' => $weight]) { + $this->assertInstanceOf(Definition::class, $args[2][$i]); + $this->assertSame(Arc::class, $args[2][$i]->getClass()); + $arcArgs = array_values($args[2][$i]->getArguments()); + $this->assertSame($place, $arcArgs[0]); + $this->assertSame($weight, $arcArgs[1]); + } + } + + public function testWorkflowEnumPlaces() + { + $container = $this->createContainerFromFile('workflow_enum_places'); + + $workflowDefinition = $container->getDefinition('state_machine.enum.definition'); + $this->assertSame(['a', 'b', 'c'], $workflowDefinition->getArgument(0)); + $this->assertTransitionReference( + $container, + '.state_machine.enum.transition.0', + 'one', + [['place' => 'a', 'weight' => 1]], + [['place' => 'b', 'weight' => 1]], + $workflowDefinition->getArgument(1)[0], + ); + + $this->assertTransitionReference( + $container, + '.state_machine.enum.transition.1', + 'two', + [['place' => 'b', 'weight' => 1]], + [['place' => 'c', 'weight' => 1]], + $workflowDefinition->getArgument(1)[1], + ); + } + + public function testWorkflowGlobPlaces() + { + $container = $this->createContainerFromFile('workflow_glob_places'); + + $workflowDefinition = $container->getDefinition('state_machine.enum.definition'); + $this->assertSame(['a', 'b', 'c'], $workflowDefinition->getArgument(0)); } public function testWorkflowGuardExpressions() @@ -594,7 +703,12 @@ public function testWorkflowTransitionsPerformNoDeepMerging() } $this->assertCount(1, $transitions); - $this->assertSame(['base_transition', ['middle'], ['alternative']], $transitions[0]); + $this->assertTransition( + 'base_transition', + [['place' => 'middle', 'weight' => 1]], + [['place' => 'alternative', 'weight' => 1]], + $transitions[0], + ); } public function testEnabledPhpErrorsConfig() @@ -633,8 +747,8 @@ public function testPhpErrorsWithLogLevels() $definition = $container->getDefinition('debug.error_handler_configurator'); $this->assertEquals(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), $definition->getArgument(0)); $this->assertSame([ - \E_NOTICE => \Psr\Log\LogLevel::ERROR, - \E_WARNING => \Psr\Log\LogLevel::ERROR, + \E_NOTICE => LogLevel::ERROR, + \E_WARNING => LogLevel::ERROR, ], $definition->getArgument(1)); } @@ -645,35 +759,35 @@ public function testExceptionsConfig() $configuration = $container->getDefinition('exception_listener')->getArgument(3); $this->assertSame([ - \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class, - \Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class, - \Symfony\Component\HttpKernel\Exception\ConflictHttpException::class, - \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class, + BadRequestHttpException::class, + NotFoundHttpException::class, + ConflictHttpException::class, + ServiceUnavailableHttpException::class, ], array_keys($configuration)); $this->assertEqualsCanonicalizing([ 'log_channel' => null, 'log_level' => 'info', 'status_code' => 422, - ], $configuration[\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class]); + ], $configuration[BadRequestHttpException::class]); $this->assertEqualsCanonicalizing([ 'log_channel' => null, 'log_level' => 'info', 'status_code' => null, - ], $configuration[\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class]); + ], $configuration[NotFoundHttpException::class]); $this->assertEqualsCanonicalizing([ 'log_channel' => null, 'log_level' => 'info', 'status_code' => null, - ], $configuration[\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class]); + ], $configuration[ConflictHttpException::class]); $this->assertEqualsCanonicalizing([ 'log_channel' => null, 'log_level' => null, 'status_code' => 500, - ], $configuration[\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class]); + ], $configuration[ServiceUnavailableHttpException::class]); } public function testRouter() @@ -1105,6 +1219,7 @@ public function testMessengerWithMultipleBusesWithoutDeduplicateMiddleware() $this->assertTrue($container->has('messenger.bus.commands')); $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); $this->assertEquals([ + ['id' => 'add_default_stamps_middleware'], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], @@ -1115,6 +1230,7 @@ public function testMessengerWithMultipleBusesWithoutDeduplicateMiddleware() $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); $this->assertEquals([ + ['id' => 'add_default_stamps_middleware'], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], @@ -1147,6 +1263,7 @@ public function testMessengerWithAddBusNameStampMiddleware() $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); $this->assertEquals([ + ['id' => 'add_default_stamps_middleware'], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], @@ -1167,6 +1284,7 @@ public function testMessengerWithMultipleBusesWithDeduplicateMiddleware() $this->assertTrue($container->has('messenger.bus.commands')); $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); $this->assertEquals([ + ['id' => 'add_default_stamps_middleware'], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], @@ -1178,6 +1296,7 @@ public function testMessengerWithMultipleBusesWithDeduplicateMiddleware() $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); $this->assertEquals([ + ['id' => 'add_default_stamps_middleware'], ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], @@ -1341,16 +1460,17 @@ public function testValidation() $projectDir = $container->getParameter('kernel.project_dir'); $ref = new \ReflectionClass(Form::class); - $xmlMappings = [ - \dirname($ref->getFileName()).'/Resources/config/validation.xml', - strtr($projectDir.'/config/validator/foo.xml', '/', \DIRECTORY_SEPARATOR), - ]; + $xmlMappings = []; + if (!$ref->getAttributes(Traverse::class)) { + $xmlMappings[] = \dirname($ref->getFileName()).'/Resources/config/validation.xml'; + } + $xmlMappings[] = strtr($projectDir.'/config/validator/foo.xml', '/', \DIRECTORY_SEPARATOR); $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $annotations = !class_exists(FullStack::class); + $attributes = !class_exists(FullStack::class); - $this->assertCount($annotations ? 8 : 7, $calls); + $this->assertCount($attributes ? 8 : 7, $calls); $this->assertSame('setConstraintValidatorFactory', $calls[0][0]); $this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]); $this->assertSame('setGroupProviderLocator', $calls[1][0]); @@ -1362,7 +1482,7 @@ public function testValidation() $this->assertSame('addXmlMappings', $calls[4][0]); $this->assertSame([$xmlMappings], $calls[4][1]); $i = 4; - if ($annotations) { + if ($attributes) { $this->assertSame('enableAttributeMapping', $calls[++$i][0]); } $this->assertSame('addMethodMapping', $calls[++$i][0]); @@ -1437,15 +1557,19 @@ public function testValidationPaths() $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[8][1]); $xmlMappings = $calls[4][1][0]; - $this->assertCount(3, $xmlMappings); - try { - // Testing symfony/symfony - $this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]); - } catch (\Exception $e) { - // Testing symfony/framework-bundle with deps=high - $this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]); + + if (!(new \ReflectionClass(Form::class))->getAttributes(Traverse::class)) { + try { + // Testing symfony/symfony + $this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]); + } catch (\Exception $e) { + // Testing symfony/framework-bundle with deps=high + $this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]); + } + array_shift($xmlMappings); } - $this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[1]); + $this->assertCount(2, $xmlMappings); + $this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[0]); $yamlMappings = $calls[5][1][0]; $this->assertCount(1, $yamlMappings); @@ -1463,16 +1587,19 @@ public function testValidationPathsUsingCustomBundlePath() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); $xmlMappings = $calls[4][1][0]; - $this->assertCount(3, $xmlMappings); - - try { - // Testing symfony/symfony - $this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]); - } catch (\Exception $e) { - // Testing symfony/framework-bundle with deps=high - $this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]); + + if (!(new \ReflectionClass(Form::class))->getAttributes(Traverse::class)) { + try { + // Testing symfony/symfony + $this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]); + } catch (\Exception $e) { + // Testing symfony/framework-bundle with deps=high + $this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]); + } + array_shift($xmlMappings); } - $this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[1]); + $this->assertCount(2, $xmlMappings); + $this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[0]); $yamlMappings = $calls[5][1][0]; $this->assertCount(1, $yamlMappings); @@ -1519,7 +1646,6 @@ public function testValidationMapping() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); $this->assertSame('addXmlMappings', $calls[4][0]); - $this->assertCount(3, $calls[4][1][0]); $this->assertSame('addYamlMappings', $calls[5][0]); $this->assertCount(3, $calls[5][1][0]); @@ -1592,7 +1718,7 @@ public function testSerializerEnabled() $argument = $container->getDefinition('serializer.mapping.chain_loader')->getArgument(0); $this->assertCount(2, $argument); - $this->assertEquals(AttributeLoader::class, $argument[0]->getClass()); + $this->assertEquals(new Reference('serializer.mapping.attribute_loader'), $argument[0]); $this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.name_converter.metadata_aware')->getArgument(1)); $this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3)); } @@ -1630,7 +1756,7 @@ public function testRegisterSerializerExtractor() $serializerExtractorDefinition = $container->getDefinition('property_info.serializer_extractor'); $this->assertEquals('serializer.mapping.class_metadata_factory', $serializerExtractorDefinition->getArgument(0)->__toString()); - $this->assertTrue(!$serializerExtractorDefinition->isPublic() || $serializerExtractorDefinition->isPrivate()); + $this->assertTrue($serializerExtractorDefinition->isPrivate()); $tag = $serializerExtractorDefinition->getTag('property_info.list_extractor'); $this->assertEquals(['priority' => -999], $tag[0]); } @@ -1782,6 +1908,7 @@ public function testSerializerMapping() $projectDir = $container->getParameter('kernel.project_dir'); $configDir = __DIR__.'/Fixtures/TestBundle/Resources/config'; $expectedLoaders = [ + new Reference('serializer.mapping.attribute_loader'), new Definition(XmlFileLoader::class, [$configDir.'/serialization.xml']), new Definition(YamlFileLoader::class, [$configDir.'/serialization.yml']), new Definition(YamlFileLoader::class, [$projectDir.'/config/serializer/foo.yml']), @@ -1791,15 +1918,15 @@ public function testSerializerMapping() new Definition(YamlFileLoader::class, [$configDir.'/serializer_mapping/serialization.yaml']), ]; - foreach ($expectedLoaders as $definition) { - if (is_file($arg = $definition->getArgument(0))) { - $definition->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR)); + foreach ($expectedLoaders as $loader) { + if ($loader instanceof Definition && is_file($arg = $loader->getArgument(0))) { + $loader->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR)); } } $loaders = $container->getDefinition('serializer.mapping.chain_loader')->getArgument(0); foreach ($loaders as $loader) { - if (is_file($arg = $loader->getArgument(0))) { + if ($loader instanceof Definition && is_file($arg = $loader->getArgument(0))) { $loader->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR)); } } @@ -1969,9 +2096,7 @@ public function testRedisTagAwareAdapter() } } - /** - * @dataProvider appRedisTagAwareConfigProvider - */ + #[DataProvider('appRedisTagAwareConfigProvider')] public function testAppRedisTagAwareAdapter(string $configFile) { $container = $this->createContainerFromFile($configFile); @@ -2015,9 +2140,7 @@ public function testCacheTaggableTagAppliedToPools() } } - /** - * @dataProvider appRedisTagAwareConfigProvider - */ + #[DataProvider('appRedisTagAwareConfigProvider')] public function testCacheTaggableTagAppliedToRedisAwareAppPool(string $configFile) { $container = $this->createContainerFromFile($configFile); @@ -2150,6 +2273,39 @@ public function testHttpClientOverrideDefaultOptions() $this->assertEquals($expected, $container->getDefinition('foo')->getArgument(2)); } + public function testCachingHttpClient() + { + if (!class_exists(ChunkCacheItemNotFoundException::class)) { + $this->expectException(LogicException::class); + } + + $container = $this->createContainerFromFile('http_client_caching'); + + $this->assertTrue($container->hasDefinition('http_client.caching')); + $definition = $container->getDefinition('http_client.caching'); + $this->assertSame(CachingHttpClient::class, $definition->getClass()); + $this->assertSame('http_client', $definition->getDecoratedService()[0]); + $this->assertCount(5, $arguments = $definition->getArguments()); + $this->assertInstanceOf(Reference::class, $arguments[0]); + $this->assertSame('http_client.caching.inner', (string) $arguments[0]); + $this->assertInstanceOf(Reference::class, $arguments[1]); + $this->assertSame('foo', (string) $arguments[1]); + $this->assertArrayHasKey('headers', $arguments[2]); + $this->assertSame(['X-powered' => 'PHP'], $arguments[2]['headers']); + $this->assertFalse($arguments[3]); + $this->assertSame(2, $arguments[4]); + + $this->assertTrue($container->hasDefinition('bar.caching')); + $definition = $container->getDefinition('bar.caching'); + $this->assertSame(CachingHttpClient::class, $definition->getClass()); + $this->assertSame('bar', $definition->getDecoratedService()[0]); + $arguments = $definition->getArguments(); + $this->assertInstanceOf(Reference::class, $arguments[0]); + $this->assertSame('bar.caching.inner', (string) $arguments[0]); + $this->assertInstanceOf(Reference::class, $arguments[1]); + $this->assertSame('baz', (string) $arguments[1]); + } + public function testHttpClientRetry() { $container = $this->createContainerFromFile('http_client_retry'); @@ -2257,9 +2413,7 @@ public static function provideMailer(): iterable ]; } - /** - * @dataProvider provideMailer - */ + #[DataProvider('provideMailer')] public function testMailer(string $configFile, array $expectedTransports, array $expectedRecipients, array $expectedAllowedRecipients) { $container = $this->createContainerFromFile($configFile); diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index c4f67c2f1..8e55f9a8a 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -71,36 +72,6 @@ public function testAssetPackageCannotHavePathAndUrl() }); } - public function testWorkflowValidationPlacesIsArray() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The "places" option must be an array in workflow configuration.'); - $this->createContainerFromClosure(function ($container) { - $container->loadFromExtension('framework', [ - 'workflows' => [ - 'article' => [ - 'places' => null, - ], - ], - ]); - }); - } - - public function testWorkflowValidationTransitonsIsArray() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The "transitions" option must be an array in workflow configuration.'); - $this->createContainerFromClosure(function ($container) { - $container->loadFromExtension('framework', [ - 'workflows' => [ - 'article' => [ - 'transitions' => null, - ], - ], - ]); - }); - } - public function testWorkflowValidationStateMachine() { $this->expectException(InvalidDefinitionException::class); @@ -135,9 +106,7 @@ public function testWorkflowValidationStateMachine() }); } - /** - * @dataProvider provideWorkflowValidationCustomTests - */ + #[DataProvider('provideWorkflowValidationCustomTests')] public function testWorkflowValidationCustomBroken(string $class, string $message) { $this->expectException(InvalidConfigurationException::class); @@ -431,9 +400,7 @@ public function testRateLimiterCompoundPolicyInvalidLimiters() }); } - /** - * @dataProvider emailValidationModeProvider - */ + #[DataProvider('emailValidationModeProvider')] public function testValidatorEmailValidationMode(string $mode) { $this->expectNotToPerformAssertions(); diff --git a/Tests/DependencyInjection/XmlFrameworkExtensionTest.php b/Tests/DependencyInjection/XmlFrameworkExtensionTest.php index 1b2eb668a..083e5e736 100644 --- a/Tests/DependencyInjection/XmlFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/XmlFrameworkExtensionTest.php @@ -11,10 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +#[IgnoreDeprecations] +#[Group('legacy')] class XmlFrameworkExtensionTest extends FrameworkExtensionTestCase { protected function loadFromFile(ContainerBuilder $container, $file) @@ -59,4 +63,9 @@ public function testAssetMapper() $definition = $container->getDefinition('asset_mapper.compiler.css_asset_url_compiler'); $this->assertSame('strict', $definition->getArgument(0)); } + + public function testWorkflowEnumPlaces() + { + $this->markTestSkipped('XML configuration does not allow to reference enums.'); + } } diff --git a/Tests/Fixtures/Descriptor/empty_route_collection.json b/Tests/Fixtures/Descriptor/empty_route_collection.json new file mode 100644 index 000000000..7dd438752 --- /dev/null +++ b/Tests/Fixtures/Descriptor/empty_route_collection.json @@ -0,0 +1,2 @@ +[] + diff --git a/Tests/Fixtures/Descriptor/empty_route_collection.md b/Tests/Fixtures/Descriptor/empty_route_collection.md new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/Fixtures/Descriptor/empty_route_collection.txt b/Tests/Fixtures/Descriptor/empty_route_collection.txt new file mode 100644 index 000000000..de24cefb8 --- /dev/null +++ b/Tests/Fixtures/Descriptor/empty_route_collection.txt @@ -0,0 +1,4 @@ + ------ -------- ------ +  Name   Method   Path  + ------ -------- ------ + diff --git a/Tests/Fixtures/Descriptor/empty_route_collection.xml b/Tests/Fixtures/Descriptor/empty_route_collection.xml new file mode 100644 index 000000000..ea7517e16 --- /dev/null +++ b/Tests/Fixtures/Descriptor/empty_route_collection.xml @@ -0,0 +1,3 @@ + + + diff --git a/Tests/Fixtures/Descriptor/route_1.txt b/Tests/Fixtures/Descriptor/route_1.txt index 9814273b7..bb70cea61 100644 --- a/Tests/Fixtures/Descriptor/route_1.txt +++ b/Tests/Fixtures/Descriptor/route_1.txt @@ -7,7 +7,7 @@ | Host | localhost | | Host Regex | #HOST_REGEX# | | Scheme | http|https | -| Method | GET|HEAD | +| Method | GET|HEAD | | Requirements | name: [a-z]+ | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | name: Joseph | diff --git a/Tests/Fixtures/Descriptor/route_1_link.txt b/Tests/Fixtures/Descriptor/route_1_link.txt index ad7a4c8c8..827873de5 100644 --- a/Tests/Fixtures/Descriptor/route_1_link.txt +++ b/Tests/Fixtures/Descriptor/route_1_link.txt @@ -7,10 +7,10 @@ | Host | localhost | | Host Regex | #HOST_REGEX# | | Scheme | http|https | -| Method | GET|HEAD | +| 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;;\ | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=59\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | | | name: Joseph | | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | | | opt1: val1 | diff --git a/Tests/Fixtures/Descriptor/route_2.txt b/Tests/Fixtures/Descriptor/route_2.txt index 533409d40..7bdbb83a0 100644 --- a/Tests/Fixtures/Descriptor/route_2.txt +++ b/Tests/Fixtures/Descriptor/route_2.txt @@ -7,7 +7,7 @@ | Host | localhost | | Host Regex | #HOST_REGEX# | | Scheme | http|https | -| Method | PUT|POST | +| Method | PUT|POST | | Requirements | NO CUSTOM | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | NONE | diff --git a/Tests/Fixtures/Descriptor/route_2_link.txt b/Tests/Fixtures/Descriptor/route_2_link.txt index 8e3fe4ca7..25941f651 100644 --- a/Tests/Fixtures/Descriptor/route_2_link.txt +++ b/Tests/Fixtures/Descriptor/route_2_link.txt @@ -7,10 +7,10 @@ | Host | localhost | | Host Regex | #HOST_REGEX# | | Scheme | http|https | -| Method | PUT|POST | +| 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;;\ | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=59\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | | | opt1: val1 | | | opt2: val2 | diff --git a/Tests/Fixtures/Descriptor/route_collection_1.json b/Tests/Fixtures/Descriptor/route_collection_1.json index 200108a16..b5d584c95 100644 --- a/Tests/Fixtures/Descriptor/route_collection_1.json +++ b/Tests/Fixtures/Descriptor/route_collection_1.json @@ -37,3 +37,4 @@ "condition": "context.getMethod() in ['GET', 'HEAD', 'POST']" } } + diff --git a/Tests/Fixtures/Descriptor/route_collection_1.md b/Tests/Fixtures/Descriptor/route_collection_1.md index 432001f02..64b38953b 100644 --- a/Tests/Fixtures/Descriptor/route_collection_1.md +++ b/Tests/Fixtures/Descriptor/route_collection_1.md @@ -34,5 +34,4 @@ route_2 - `compiler_class`: Symfony\Component\Routing\RouteCompiler - `opt1`: val1 - `opt2`: val2 -- Condition: context.getMethod() in ['GET', 'HEAD', 'POST'] - +- Condition: context.getMethod() in ['GET', 'HEAD', 'POST'] \ No newline at end of file diff --git a/Tests/Fixtures/Descriptor/route_collection_1.txt b/Tests/Fixtures/Descriptor/route_collection_1.txt index 9d0656232..b787a2d4b 100644 --- a/Tests/Fixtures/Descriptor/route_collection_1.txt +++ b/Tests/Fixtures/Descriptor/route_collection_1.txt @@ -1,7 +1,7 @@ --------- ---------- ------------ ----------- ---------------  Name   Method   Scheme   Host   Path  --------- ---------- ------------ ----------- --------------- - route_1 GET|HEAD http|https localhost /hello/{name} - route_2 PUT|POST http|https localhost /name/add + route_1 GET|HEAD http|https localhost /hello/{name} + route_2 PUT|POST http|https localhost /name/add --------- ---------- ------------ ----------- --------------- diff --git a/Tests/Fixtures/Descriptor/route_collection_1.xml b/Tests/Fixtures/Descriptor/route_collection_1.xml index 6a07e0596..c36826d86 100644 --- a/Tests/Fixtures/Descriptor/route_collection_1.xml +++ b/Tests/Fixtures/Descriptor/route_collection_1.xml @@ -34,3 +34,4 @@ context.getMethod() in ['GET', 'HEAD', 'POST'] + diff --git a/Tests/Fixtures/Descriptor/route_collection_2.txt b/Tests/Fixtures/Descriptor/route_collection_2.txt index a9f9ee21b..e511a2857 100644 --- a/Tests/Fixtures/Descriptor/route_collection_2.txt +++ b/Tests/Fixtures/Descriptor/route_collection_2.txt @@ -1,7 +1,7 @@ --------- ---------- ------------ ----------- ---------------  Name   Method   Scheme   Host   Path  --------- ---------- ------------ ----------- --------------- - route_1 GET|HEAD http|https localhost /hello/{name} - route_3 ANY http|https localhost /other/route + route_1 GET|HEAD http|https localhost /hello/{name} + route_3 ANY http|https localhost /other/route --------- ---------- ------------ ----------- --------------- diff --git a/Tests/Fixtures/Descriptor/route_collection_3.txt b/Tests/Fixtures/Descriptor/route_collection_3.txt index 8822b3c40..1d89756d2 100644 --- a/Tests/Fixtures/Descriptor/route_collection_3.txt +++ b/Tests/Fixtures/Descriptor/route_collection_3.txt @@ -1,6 +1,6 @@ --------- ---------- ------------ ----------- -----------  Name   Method   Scheme   Host   Path  --------- ---------- ------------ ----------- ----------- - route_2 PUT|POST http|https localhost /name/add + route_2 PUT|POST http|https localhost /name/add --------- ---------- ------------ ----------- ----------- diff --git a/Tests/Fixtures/Descriptor/route_with_generic_host.json b/Tests/Fixtures/Descriptor/route_with_generic_host.json new file mode 100644 index 000000000..bc002e61d --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_host.json @@ -0,0 +1,18 @@ +{ + "some_route": { + "path": "\/some-route", + "pathRegex": "#PATH_REGEX#", + "host": "ANY", + "hostRegex": "", + "scheme": "https", + "method": "ANY", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub", + "defaults": { + "_controller": "Controller" + }, + "requirements": "NO CUSTOM", + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler" + } + } +} diff --git a/Tests/Fixtures/Descriptor/route_with_generic_host.md b/Tests/Fixtures/Descriptor/route_with_generic_host.md new file mode 100644 index 000000000..41466a164 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_host.md @@ -0,0 +1,15 @@ +some_route +---------- + +- Path: /some-route +- Path Regex: #PATH_REGEX# +- Host: ANY +- Host Regex: +- Scheme: https +- Method: ANY +- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub +- Defaults: + - `_controller`: Controller +- Requirements: NO CUSTOM +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler \ No newline at end of file diff --git a/Tests/Fixtures/Descriptor/route_with_generic_host.txt b/Tests/Fixtures/Descriptor/route_with_generic_host.txt new file mode 100644 index 000000000..6c34fc464 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_host.txt @@ -0,0 +1,6 @@ + ------------ -------- -------- ------------- +  Name   Method   Scheme   Path  + ------------ -------- -------- ------------- + some_route ANY https /some-route + ------------ -------- -------- ------------- + diff --git a/Tests/Fixtures/Descriptor/route_with_generic_host.xml b/Tests/Fixtures/Descriptor/route_with_generic_host.xml new file mode 100644 index 000000000..de9930fc5 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_host.xml @@ -0,0 +1,14 @@ + + + + /some-route + https + + Controller + + + + + + + diff --git a/Tests/Fixtures/Descriptor/route_with_generic_scheme.json b/Tests/Fixtures/Descriptor/route_with_generic_scheme.json new file mode 100644 index 000000000..811c07773 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_scheme.json @@ -0,0 +1,18 @@ +{ + "some_route_with_host": { + "path": "\/some-route", + "pathRegex": "#PATH_REGEX#", + "host": "symfony.com", + "hostRegex": "#HOST_REGEX#", + "scheme": "ANY", + "method": "ANY", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub", + "defaults": { + "_controller": "strpos" + }, + "requirements": "NO CUSTOM", + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler" + } + } +} diff --git a/Tests/Fixtures/Descriptor/route_with_generic_scheme.md b/Tests/Fixtures/Descriptor/route_with_generic_scheme.md new file mode 100644 index 000000000..8a8bad6ea --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_scheme.md @@ -0,0 +1,15 @@ +some_route_with_host +-------------------- + +- Path: /some-route +- Path Regex: #PATH_REGEX# +- Host: symfony.com +- Host Regex: #HOST_REGEX# +- Scheme: ANY +- Method: ANY +- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub +- Defaults: + - `_controller`: strpos +- Requirements: NO CUSTOM +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler diff --git a/Tests/Fixtures/Descriptor/route_with_generic_scheme.txt b/Tests/Fixtures/Descriptor/route_with_generic_scheme.txt new file mode 100644 index 000000000..cb83e325a --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_scheme.txt @@ -0,0 +1,6 @@ + ---------------------- -------- ------------- ------------- +  Name   Method   Host   Path  + ---------------------- -------- ------------- ------------- + some_route_with_host ANY symfony.com /some-route + ---------------------- -------- ------------- ------------- + diff --git a/Tests/Fixtures/Descriptor/route_with_generic_scheme.xml b/Tests/Fixtures/Descriptor/route_with_generic_scheme.xml new file mode 100644 index 000000000..fe7d8da8d --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_with_generic_scheme.xml @@ -0,0 +1,13 @@ + + + + /some-route + symfony.com + + strpos + + + + + + diff --git a/Tests/Fixtures/Validation/Resources/categories.yml b/Tests/Fixtures/Validation/Resources/categories.yml index 5a0f0da28..29409b9de 100644 --- a/Tests/Fixtures/Validation/Resources/categories.yml +++ b/Tests/Fixtures/Validation/Resources/categories.yml @@ -1,9 +1,11 @@ Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Category: properties: id: - - Type: int + - Type: + type: int Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\SubCategory: properties: id: - - Type: int + - Type: + type: int diff --git a/Tests/Fixtures/reference.php b/Tests/Fixtures/reference.php new file mode 100644 index 000000000..4830b3e1f --- /dev/null +++ b/Tests/Fixtures/reference.php @@ -0,0 +1,250 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator|ExpressionConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: mixed, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: InstanceofType, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type TestConfig = array{ + * enabled?: scalar|null, // Default: false + * options?: array{ + * name?: scalar|null, + * count?: int, + * }, + * } + */ +final class App extends AppReference +{ + /** + * @param array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * test?: TestConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * test?: TestConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * test?: TestConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * test?: TestConfig, + * }, + * ..., + * }> + * } $config + */ + public static function config(array $config): array + { + return parent::config($config); + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'attributes', + * 'type' => 'tagged_services', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes extends RoutesReference +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return parent::config($config); + } +} diff --git a/Tests/Functional/AbstractAttributeRoutingTestCase.php b/Tests/Functional/AbstractAttributeRoutingTestCase.php index 5166c8dda..842d7268f 100644 --- a/Tests/Functional/AbstractAttributeRoutingTestCase.php +++ b/Tests/Functional/AbstractAttributeRoutingTestCase.php @@ -11,13 +11,12 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\HttpFoundation\Request; abstract class AbstractAttributeRoutingTestCase extends AbstractWebTestCase { - /** - * @dataProvider getRoutes - */ + #[DataProvider('getRoutes')] public function testAnnotatedController(string $path, string $expectedValue) { $client = $this->createClient(['test_case' => $this->getTestCaseApp(), 'root_config' => 'config.yml']); diff --git a/Tests/Functional/ApiAttributesTest.php b/Tests/Functional/ApiAttributesTest.php index 4848976ae..79c8a704b 100644 --- a/Tests/Functional/ApiAttributesTest.php +++ b/Tests/Functional/ApiAttributesTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -21,9 +22,7 @@ class ApiAttributesTest extends AbstractWebTestCase { - /** - * @dataProvider mapQueryStringProvider - */ + #[DataProvider('mapQueryStringProvider')] public function testMapQueryString(string $uri, array $query, string $expectedResponse, int $expectedStatusCode) { $client = self::createClient(['test_case' => 'ApiAttributesTest']); @@ -146,24 +145,24 @@ public static function mapQueryStringProvider(): iterable ]; $expectedResponse = <<<'JSON' - { - "type": "https:\/\/symfony.com\/errors\/validation", - "title": "Validation Failed", - "status": 404, - "detail": "filter: This value should be of type Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter.", - "violations": [ - { - "parameters": { - "hint": "Failed to create object because the class misses the \"filter\" property.", - "{{ type }}": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter" - }, - "propertyPath": "filter", - "template": "This value should be of type {{ type }}.", - "title": "This value should be of type Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter." - } - ] - } - JSON; + { + "type": "https:\/\/symfony.com\/errors\/validation", + "title": "Validation Failed", + "status": 404, + "detail": "filter: This value should be of type Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter.", + "violations": [ + { + "parameters": { + "hint": "Failed to create object because the class misses the \"filter\" property.", + "{{ type }}": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter" + }, + "propertyPath": "filter", + "template": "This value should be of type {{ type }}.", + "title": "This value should be of type Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Filter." + } + ] + } + JSON; yield 'empty query string mapping non-nullable attribute without default value' => [ 'uri' => '/map-query-string-to-non-nullable-attribute-without-default-value.json', @@ -214,9 +213,7 @@ public static function mapQueryStringProvider(): iterable ]; } - /** - * @dataProvider mapRequestPayloadProvider - */ + #[DataProvider('mapRequestPayloadProvider')] public function testMapRequestPayload(string $uri, string $format, array $parameters, ?string $content, callable $responseAssertion, int $expectedStatusCode) { $client = self::createClient(['test_case' => 'ApiAttributesTest']); @@ -603,7 +600,7 @@ public static function mapRequestPayloadProvider(): iterable self::assertIsArray($json['violations'] ?? null); self::assertCount(1, $json['violations']); self::assertSame('approved', $json['violations'][0]['propertyPath'] ?? null); -}, + }, 'expectedStatusCode' => 422, ]; @@ -947,11 +944,11 @@ public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $reque return new Response( << - {$body->comment} - {$body->approved} - - XML + + {$body->comment} + {$body->approved} + + XML ); } } @@ -966,11 +963,11 @@ public function __invoke(Request $request, #[MapRequestPayload] RequestBody $bod return new Response( << - {$body->comment} - {$body->approved} - - XML + + {$body->comment} + {$body->approved} + + XML ); } } @@ -985,11 +982,11 @@ public function __invoke(Request $request, #[MapRequestPayload] RequestBody $bod return new Response( << - {$body->comment} - {$body->approved} - - XML + + {$body->comment} + {$body->approved} + + XML ); } } diff --git a/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php b/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php index 560f84e62..e6db5e225 100644 --- a/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php +++ b/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php @@ -20,11 +20,17 @@ public function load(array $configs, ContainerBuilder $container): void { } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getNamespace(): string { return ''; } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getXsdValidationBasePath(): string|false { return false; diff --git a/Tests/Functional/BundlePathsTest.php b/Tests/Functional/BundlePathsTest.php index a06803434..45663f0bf 100644 --- a/Tests/Functional/BundlePathsTest.php +++ b/Tests/Functional/BundlePathsTest.php @@ -28,7 +28,7 @@ public function testBundlePublicDir() $fs = new Filesystem(); $fs->remove($projectDir); $fs->mkdir($projectDir.'/public'); - $command = (new Application($kernel))->add(new AssetsInstallCommand($fs, $projectDir)); + $command = (new Application($kernel))->addCommand(new AssetsInstallCommand($fs, $projectDir)); $exitCode = (new CommandTester($command))->execute(['target' => $projectDir.'/public']); $this->assertSame(0, $exitCode); diff --git a/Tests/Functional/CachePoolClearCommandTest.php b/Tests/Functional/CachePoolClearCommandTest.php index dbd78645d..53e8b5c48 100644 --- a/Tests/Functional/CachePoolClearCommandTest.php +++ b/Tests/Functional/CachePoolClearCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Group; use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -19,9 +20,7 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\Finder\SplFileInfo; -/** - * @group functional - */ +#[Group('functional')] class CachePoolClearCommandTest extends AbstractWebTestCase { protected function setUp(): void @@ -146,7 +145,7 @@ public function testExcludedPool() private function createCommandTester(?array $poolNames = null) { $application = new Application(static::$kernel); - $application->add(new CachePoolClearCommand(static::getContainer()->get('cache.global_clearer'), $poolNames)); + $application->addCommand(new CachePoolClearCommand(static::getContainer()->get('cache.global_clearer'), $poolNames)); return new CommandTester($application->find('cache:pool:clear')); } diff --git a/Tests/Functional/CachePoolListCommandTest.php b/Tests/Functional/CachePoolListCommandTest.php index 8e9061845..6dcbc4294 100644 --- a/Tests/Functional/CachePoolListCommandTest.php +++ b/Tests/Functional/CachePoolListCommandTest.php @@ -11,13 +11,12 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Group; use Symfony\Bundle\FrameworkBundle\Command\CachePoolListCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -/** - * @group functional - */ +#[Group('functional')] class CachePoolListCommandTest extends AbstractWebTestCase { protected function setUp(): void @@ -46,7 +45,7 @@ public function testEmptyList() private function createCommandTester(array $poolNames) { $application = new Application(static::$kernel); - $application->add(new CachePoolListCommand($poolNames)); + $application->addCommand(new CachePoolListCommand($poolNames)); return new CommandTester($application->find('cache:pool:list')); } diff --git a/Tests/Functional/CachePoolsTest.php b/Tests/Functional/CachePoolsTest.php index 23f4a116e..64829949a 100644 --- a/Tests/Functional/CachePoolsTest.php +++ b/Tests/Functional/CachePoolsTest.php @@ -11,6 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use PHPUnit\Framework\Error\Warning; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; @@ -24,18 +27,15 @@ public function testCachePools() $this->doTestCachePools([], AdapterInterface::class); } - /** - * @requires extension redis - * - * @group integration - */ + #[RequiresPhpExtension('redis')] + #[Group('integration')] public function testRedisCachePools() { $this->skipIfRedisUnavailable(); try { $this->doTestCachePools(['root_config' => 'redis_config.yml', 'environment' => 'redis_cache'], RedisAdapter::class); - } catch (\PHPUnit\Framework\Error\Warning $e) { + } catch (Warning $e) { if (!str_starts_with($e->getMessage(), 'unable to connect to')) { throw $e; } @@ -48,18 +48,15 @@ public function testRedisCachePools() } } - /** - * @requires extension redis - * - * @group integration - */ + #[RequiresPhpExtension('redis')] + #[Group('integration')] public function testRedisCustomCachePools() { $this->skipIfRedisUnavailable(); try { $this->doTestCachePools(['root_config' => 'redis_custom_config.yml', 'environment' => 'custom_redis_cache'], RedisAdapter::class); - } catch (\PHPUnit\Framework\Error\Warning $e) { + } catch (Warning $e) { if (!str_starts_with($e->getMessage(), 'unable to connect to')) { throw $e; } diff --git a/Tests/Functional/ConfigDebugCommandTest.php b/Tests/Functional/ConfigDebugCommandTest.php index 2c47121c1..ea4ee0788 100644 --- a/Tests/Functional/ConfigDebugCommandTest.php +++ b/Tests/Functional/ConfigDebugCommandTest.php @@ -11,6 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\TestWith; use Symfony\Bundle\FrameworkBundle\Command\ConfigDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Exception\InvalidArgumentException; @@ -19,15 +22,11 @@ use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; -/** - * @group functional - */ +#[Group('functional')] class ConfigDebugCommandTest extends AbstractWebTestCase { - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testShowList(bool $debug) { $tester = $this->createCommandTester($debug); @@ -44,10 +43,8 @@ public function testShowList(bool $debug) $this->assertStringContainsString(' test_dump', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpKernelExtension(bool $debug) { $tester = $this->createCommandTester($debug); @@ -58,10 +55,8 @@ public function testDumpKernelExtension(bool $debug) $this->assertStringContainsString(' foo: bar', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpBundleName(bool $debug) { $tester = $this->createCommandTester($debug); @@ -71,10 +66,8 @@ public function testDumpBundleName(bool $debug) $this->assertStringContainsString('custom: foo', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpBundleOption(bool $debug) { $tester = $this->createCommandTester($debug); @@ -84,10 +77,8 @@ public function testDumpBundleOption(bool $debug) $this->assertStringContainsString('foo', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpWithoutTitleIsValidJson(bool $debug) { $tester = $this->createCommandTester($debug); @@ -97,10 +88,8 @@ public function testDumpWithoutTitleIsValidJson(bool $debug) $this->assertJson($tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpWithUnsupportedFormat(bool $debug) { $tester = $this->createCommandTester($debug); @@ -114,10 +103,8 @@ public function testDumpWithUnsupportedFormat(bool $debug) ]); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testParametersValuesAreResolved(bool $debug) { $tester = $this->createCommandTester($debug); @@ -128,10 +115,8 @@ public function testParametersValuesAreResolved(bool $debug) $this->assertStringContainsString('secret: test', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testParametersValuesAreFullyResolved(bool $debug) { $tester = $this->createCommandTester($debug); @@ -144,10 +129,8 @@ public function testParametersValuesAreFullyResolved(bool $debug) $this->assertStringContainsString('ide: '.($debug ? ($_ENV['SYMFONY_IDE'] ?? $_SERVER['SYMFONY_IDE'] ?? 'null') : 'null'), $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDefaultParameterValueIsResolvedIfConfigIsExisting(bool $debug) { $tester = $this->createCommandTester($debug); @@ -158,10 +141,8 @@ public function testDefaultParameterValueIsResolvedIfConfigIsExisting(bool $debu $this->assertStringContainsString(\sprintf("dsn: 'file:%s/profiler'", $kernelCacheDir), $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpExtensionConfigWithoutBundle(bool $debug) { $tester = $this->createCommandTester($debug); @@ -171,10 +152,8 @@ public function testDumpExtensionConfigWithoutBundle(bool $debug) $this->assertStringContainsString('enabled: true', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpUndefinedBundleOption(bool $debug) { $tester = $this->createCommandTester($debug); @@ -183,10 +162,8 @@ public function testDumpUndefinedBundleOption(bool $debug) $this->assertStringContainsString('Unable to find configuration for "test.foo"', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpWithPrefixedEnv(bool $debug) { $tester = $this->createCommandTester($debug); @@ -195,10 +172,8 @@ public function testDumpWithPrefixedEnv(bool $debug) $this->assertStringContainsString("cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%'", $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpFallsBackToDefaultConfigAndResolvesParameterValue(bool $debug) { $tester = $this->createCommandTester($debug); @@ -208,10 +183,8 @@ public function testDumpFallsBackToDefaultConfigAndResolvesParameterValue(bool $ $this->assertStringContainsString('foo: bar', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpFallsBackToDefaultConfigAndResolvesEnvPlaceholder(bool $debug) { $tester = $this->createCommandTester($debug); @@ -221,10 +194,8 @@ public function testDumpFallsBackToDefaultConfigAndResolvesEnvPlaceholder(bool $ $this->assertStringContainsString("baz: '%env(BAZ)%'", $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpThrowsExceptionWhenDefaultConfigFallbackIsImpossible(bool $debug) { $this->expectException(\LogicException::class); @@ -234,14 +205,12 @@ public function testDumpThrowsExceptionWhenDefaultConfigFallbackIsImpossible(boo $tester->execute(['name' => 'ExtensionWithoutConfigTestBundle']); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(bool $debug, array $input, array $expectedSuggestions) { $application = $this->createApplication($debug); - $application->add(new ConfigDebugCommand()); + $application->addCommand(new ConfigDebugCommand()); $tester = new CommandCompletionTester($application->get('debug:config')); $suggestions = $tester->complete($input); diff --git a/Tests/Functional/ConfigDumpReferenceCommandTest.php b/Tests/Functional/ConfigDumpReferenceCommandTest.php index 8f5930faa..eedcefc5b 100644 --- a/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -11,6 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\TestWith; use Symfony\Bundle\FrameworkBundle\Command\ConfigDumpReferenceCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; @@ -18,15 +21,11 @@ use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; -/** - * @group functional - */ +#[Group('functional')] class ConfigDumpReferenceCommandTest extends AbstractWebTestCase { - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testShowList(bool $debug) { $tester = $this->createCommandTester($debug); @@ -43,10 +42,8 @@ public function testShowList(bool $debug) $this->assertStringContainsString(' test_dump', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpKernelExtension(bool $debug) { $tester = $this->createCommandTester($debug); @@ -57,10 +54,8 @@ public function testDumpKernelExtension(bool $debug) $this->assertStringContainsString(' bar', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpBundleName(bool $debug) { $tester = $this->createCommandTester($debug); @@ -71,10 +66,8 @@ public function testDumpBundleName(bool $debug) $this->assertStringContainsString(' custom:', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpExtensionConfigWithoutBundle(bool $debug) { $tester = $this->createCommandTester($debug); @@ -84,10 +77,8 @@ public function testDumpExtensionConfigWithoutBundle(bool $debug) $this->assertStringContainsString('enabled: true', $tester->getDisplay()); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpAtPath(bool $debug) { $tester = $this->createCommandTester($debug); @@ -98,20 +89,19 @@ public function testDumpAtPath(bool $debug) $this->assertSame(0, $ret, 'Returns 0 in case of success'); $this->assertSame(<<<'EOL' -# Default configuration for extension with alias: "test" at path "array" -array: - child1: ~ - child2: ~ + # Default configuration for extension with alias: "test" at path "array" + array: + child1: ~ + child2: ~ -EOL - , $tester->getDisplay(true)); + EOL, + $tester->getDisplay(true) + ); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpAtPathXml(bool $debug) { $tester = $this->createCommandTester($debug); @@ -125,14 +115,23 @@ public function testDumpAtPathXml(bool $debug) $this->assertStringContainsString('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[TestWith(['yaml'])] + #[TestWith(['xml'])] + public function testDumpFrameworkBundle(string $format) + { + $tester = $this->createCommandTester(true); + $ret = $tester->execute(['name' => 'framework', '--format' => $format]); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('%env(default::SYMFONY_TRUSTED_PROXIES)%', $tester->getDisplay()); + } + + #[DataProvider('provideCompletionSuggestions')] public function testComplete(bool $debug, array $input, array $expectedSuggestions) { $application = $this->createApplication($debug); - $application->add(new ConfigDumpReferenceCommand()); + $application->addCommand(new ConfigDumpReferenceCommand()); $tester = new CommandCompletionTester($application->get('config:dump-reference')); $suggestions = $tester->complete($input); $this->assertSame($expectedSuggestions, $suggestions); diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index d21d4d113..e1e7ce084 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ContainerExcluded; @@ -18,9 +20,7 @@ use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\HttpKernel\HttpKernelInterface; -/** - * @group functional - */ +#[Group('functional')] class ContainerDebugCommandTest extends AbstractWebTestCase { public function testDumpContainerIfNotExists() @@ -113,9 +113,7 @@ public function testExcludedService() $this->assertStringNotContainsString(ContainerExcluded::class, $tester->getDisplay()); } - /** - * @dataProvider provideIgnoreBackslashWhenFindingService - */ + #[DataProvider('provideIgnoreBackslashWhenFindingService')] public function testIgnoreBackslashWhenFindingService(string $validServiceId) { static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); @@ -168,25 +166,26 @@ public function testDescribeEnvVars() $this->assertStringMatchesFormat(<<<'TXT' -Symfony Container Environment Variables -======================================= + Symfony Container Environment Variables + ======================================= - --------- ----------------- ------------%w - Name Default value Real value%w - --------- ----------------- ------------%w - JSON "[1, "2.5", 3]" n/a%w - REAL n/a "value"%w - UNKNOWN n/a n/a%w - --------- ----------------- ------------%w + --------- ----------------- ------------%w + Name Default value Real value%w + --------- ----------------- ------------%w + JSON "[1, "2.5", 3]" n/a%w + REAL n/a "value"%w + UNKNOWN n/a n/a%w + --------- ----------------- ------------%w - // Note real values might be different between web and CLI.%w + // Note real values might be different between web and CLI.%w - [WARNING] The following variables are missing:%w + [WARNING] The following variables are missing:%w - * UNKNOWN + * UNKNOWN -TXT - , $tester->getDisplay(true)); + TXT, + $tester->getDisplay(true) + ); putenv('REAL'); } @@ -214,10 +213,10 @@ public function testGetDeprecation() 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', + 'file' => '/home/hamza/project/contrib/sf/vendor/symfony/framework-bundle/Controller/Controller.php', 'line' => 17, 'trace' => [[ - 'file' => '/home/hamza/projet/contrib/sf/src/Controller/DefaultController.php', + 'file' => '/home/hamza/project/contrib/sf/src/Controller/DefaultController.php', 'line' => 9, 'function' => 'spl_autoload_call', ]], @@ -233,7 +232,7 @@ public function testGetDeprecation() $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()); + $this->assertStringContainsString('/home/hamza/project/contrib/sf/vendor/symfony/framework-bundle/Controller/Controller.php', $tester->getDisplay()); } public function testGetDeprecationNone() @@ -282,9 +281,7 @@ public static function provideIgnoreBackslashWhenFindingService(): array ]; } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions, array $notExpectedSuggestions = []) { static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); diff --git a/Tests/Functional/ContainerLintCommandTest.php b/Tests/Functional/ContainerLintCommandTest.php index f0b6b4bd5..8e50caa01 100644 --- a/Tests/Functional/ContainerLintCommandTest.php +++ b/Tests/Functional/ContainerLintCommandTest.php @@ -11,19 +11,18 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\Argument\ArgumentTrait; -/** - * @group functional - */ +#[Group('functional')] class ContainerLintCommandTest extends AbstractWebTestCase { private Application $application; - /** - * @dataProvider containerLintProvider - */ + #[DataProvider('containerLintProvider')] public function testLintContainer(string $configFile, bool $resolveEnvVars, int $expectedExitCode, string $expectedOutput) { $kernel = static::createKernel([ @@ -40,13 +39,16 @@ public function testLintContainer(string $configFile, bool $resolveEnvVars, int $this->assertStringContainsString($expectedOutput, $tester->getDisplay()); } - public static function containerLintProvider(): array + public static function containerLintProvider(): iterable { - return [ - ['escaped_percent.yml', false, 0, 'The container was linted successfully'], - ['missing_env_var.yml', false, 0, 'The container was linted successfully'], - ['missing_env_var.yml', true, 1, 'Environment variable not found: "BAR"'], - ]; + yield ['escaped_percent.yml', false, 0, 'The container was linted successfully']; + + if (trait_exists(ArgumentTrait::class)) { + yield ['escaped_percent.yml', true, 0, 'The container was linted successfully']; + } + + yield ['missing_env_var.yml', false, 0, 'The container was linted successfully']; + yield ['missing_env_var.yml', true, 1, 'Environment variable not found: "BAR"']; } private function createCommandTester(): CommandTester diff --git a/Tests/Functional/DebugAutowiringCommandTest.php b/Tests/Functional/DebugAutowiringCommandTest.php index ca11e3fae..de94a1e71 100644 --- a/Tests/Functional/DebugAutowiringCommandTest.php +++ b/Tests/Functional/DebugAutowiringCommandTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Command\DebugAutowiringCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -20,9 +22,7 @@ use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Routing\RouterInterface; -/** - * @group functional - */ +#[Group('functional')] class DebugAutowiringCommandTest extends AbstractWebTestCase { public function testBasicFunctionality() @@ -116,13 +116,11 @@ public function testNotConfusedByClassAliases() $this->assertStringContainsString(ClassAliasExampleClass::class, $tester->getDisplay()); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[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()); + $command = (new Application($kernel))->addCommand(new DebugAutowiringCommand()); $tester = new CommandCompletionTester($command); diff --git a/Tests/Functional/FragmentTest.php b/Tests/Functional/FragmentTest.php index 48d5c327a..b8cff1f48 100644 --- a/Tests/Functional/FragmentTest.php +++ b/Tests/Functional/FragmentTest.php @@ -11,11 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; + class FragmentTest extends AbstractWebTestCase { - /** - * @dataProvider getConfigs - */ + #[DataProvider('getConfigs')] public function testFragment($insulate) { $client = $this->createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]); @@ -26,15 +26,16 @@ public function testFragment($insulate) $client->request('GET', '/fragment_home'); $this->assertEquals(<<getResponse()->getContent()); + bar txt + -- + html + -- + es + -- + fr + TXT, + $client->getResponse()->getContent() + ); } public static function getConfigs() diff --git a/Tests/Functional/MailerTest.php b/Tests/Functional/MailerTest.php index 1ba71d74f..4193e3ff7 100644 --- a/Tests/Functional/MailerTest.php +++ b/Tests/Functional/MailerTest.php @@ -99,6 +99,7 @@ public function testMailerAssertions() $this->assertEmailHtmlBodyContains($email, 'Foo'); $this->assertEmailHtmlBodyNotContains($email, 'Bar'); $this->assertEmailAttachmentCount($email, 1); + $this->assertEmailAddressNotContains($email, 'To', 'thomas@symfony.com'); $email = $this->getMailerMessage($second); $this->assertEmailSubjectContains($email, 'Foo'); @@ -106,5 +107,7 @@ public function testMailerAssertions() $this->assertEmailAddressContains($email, 'To', 'fabien@symfony.com'); $this->assertEmailAddressContains($email, 'To', 'thomas@symfony.com'); $this->assertEmailAddressContains($email, 'Reply-To', 'me@symfony.com'); + $this->assertEmailAddressNotContains($email, 'To', 'helene@symfony.com'); + $this->assertEmailAddressNotContains($email, 'Reply-To', 'helene@symfony.com'); } } diff --git a/Tests/Functional/NotificationTest.php b/Tests/Functional/NotificationTest.php index 03b947a0f..7511591cb 100644 --- a/Tests/Functional/NotificationTest.php +++ b/Tests/Functional/NotificationTest.php @@ -11,11 +11,12 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\RequiresMethod; +use Symfony\Bundle\MercureBundle\MercureBundle; + final class NotificationTest extends AbstractWebTestCase { - /** - * @requires function \Symfony\Bundle\MercureBundle\MercureBundle::build - */ + #[RequiresMethod(MercureBundle::class, 'build')] public function testNotifierAssertion() { $client = $this->createClient(['test_case' => 'Notifier', 'root_config' => 'config.yml', 'debug' => true]); diff --git a/Tests/Functional/ProfilerTest.php b/Tests/Functional/ProfilerTest.php index d78259795..b5853dd1a 100644 --- a/Tests/Functional/ProfilerTest.php +++ b/Tests/Functional/ProfilerTest.php @@ -11,11 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; + class ProfilerTest extends AbstractWebTestCase { - /** - * @dataProvider getConfigs - */ + #[DataProvider('getConfigs')] public function testProfilerIsDisabled($insulate) { $client = $this->createClient(['test_case' => 'Profiler', 'root_config' => 'config.yml']); @@ -36,9 +36,7 @@ public function testProfilerIsDisabled($insulate) $this->assertNull($client->getProfile()); } - /** - * @dataProvider getConfigs - */ + #[DataProvider('getConfigs')] public function testProfilerCollectParameter($insulate) { $client = $this->createClient(['test_case' => 'ProfilerCollectParameter', 'root_config' => 'config.yml']); diff --git a/Tests/Functional/PropertyInfoTest.php b/Tests/Functional/PropertyInfoTest.php index 18cd61b08..128932311 100644 --- a/Tests/Functional/PropertyInfoTest.php +++ b/Tests/Functional/PropertyInfoTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\TypeInfo\Type; @@ -29,9 +31,8 @@ public function testPhpDocPriority() $this->assertEquals(Type::list(Type::int()), $propertyInfo->getType(Dummy::class, 'codes')); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testPhpDocPriorityLegacy() { static::bootKernel(['test_case' => 'Serializer']); diff --git a/Tests/Functional/RouterDebugCommandTest.php b/Tests/Functional/RouterDebugCommandTest.php index 614078804..910e3b6f7 100644 --- a/Tests/Functional/RouterDebugCommandTest.php +++ b/Tests/Functional/RouterDebugCommandTest.php @@ -11,13 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\TestWith; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; -/** - * @group functional - */ +#[Group('functional')] class RouterDebugCommandTest extends AbstractWebTestCase { private Application $application; @@ -89,21 +90,17 @@ public function testSearchWithThrow() $tester->execute(['name' => 'gerard'], ['interactive' => true]); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $tester = new CommandCompletionTester($this->application->get('debug:router')); $this->assertSame($expectedSuggestions, $tester->complete($input)); } - /** - * @testWith ["txt"] - * ["xml"] - * ["json"] - * ["md"] - */ + #[TestWith(['txt'])] + #[TestWith(['xml'])] + #[TestWith(['json'])] + #[TestWith(['md'])] public function testShowAliases(string $format) { $tester = $this->createCommandTester(); diff --git a/Tests/Functional/RoutingConditionServiceTest.php b/Tests/Functional/RoutingConditionServiceTest.php index 4f4caa6eb..f1f4f14cf 100644 --- a/Tests/Functional/RoutingConditionServiceTest.php +++ b/Tests/Functional/RoutingConditionServiceTest.php @@ -11,11 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; + class RoutingConditionServiceTest extends AbstractWebTestCase { - /** - * @dataProvider provideRoutes - */ + #[DataProvider('provideRoutes')] public function testCondition(int $code, string $path) { $client = static::createClient(['test_case' => 'RoutingConditionService']); diff --git a/Tests/Functional/SecurityTest.php b/Tests/Functional/SecurityTest.php index c26fa717d..ab06b5f6c 100644 --- a/Tests/Functional/SecurityTest.php +++ b/Tests/Functional/SecurityTest.php @@ -11,13 +11,12 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Security\Core\User\InMemoryUser; class SecurityTest extends AbstractWebTestCase { - /** - * @dataProvider getUsers - */ + #[DataProvider('getUsers')] public function testLoginUser(string $username, array $roles, ?string $firewallContext) { $user = new InMemoryUser($username, 'the-password', $roles); diff --git a/Tests/Functional/SessionTest.php b/Tests/Functional/SessionTest.php index 4c1b92ccf..00eb952f0 100644 --- a/Tests/Functional/SessionTest.php +++ b/Tests/Functional/SessionTest.php @@ -11,13 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\DataProvider; + class SessionTest extends AbstractWebTestCase { /** * Tests session attributes persist. - * - * @dataProvider getConfigs */ + #[DataProvider('getConfigs')] public function testWelcome($config, $insulate) { $client = $this->createClient(['test_case' => 'Session', 'root_config' => $config]); @@ -44,13 +45,26 @@ public function testWelcome($config, $insulate) // prove cleared session $crawler = $client->request('GET', '/session'); $this->assertStringContainsString('You are new here and gave no name.', $crawler->text()); + + // prepare session programatically + $session = $client->getSession(); + $session->set('name', 'drak'); + $session->save(); + + // ensure session can be saved multiple times without being reset + $session = $client->getSession(); + $session->set('foo', 'bar'); + $session->save(); + + // prove remembered name from programatically prepared session + $crawler = $client->request('GET', '/session'); + $this->assertStringContainsString('Welcome back drak, nice to meet you.', $crawler->text()); } /** * Tests flash messages work in practice. - * - * @dataProvider getConfigs */ + #[DataProvider('getConfigs')] public function testFlash($config, $insulate) { $client = $this->createClient(['test_case' => 'Session', 'root_config' => $config]); @@ -72,9 +86,8 @@ public function testFlash($config, $insulate) /** * See if two separate insulated clients can run without * polluting each other's session data. - * - * @dataProvider getConfigs */ + #[DataProvider('getConfigs')] public function testTwoClients($config, $insulate) { // start first client @@ -128,9 +141,7 @@ public function testTwoClients($config, $insulate) $this->assertStringContainsString('Welcome back client2, nice to meet you.', $crawler2->text()); } - /** - * @dataProvider getConfigs - */ + #[DataProvider('getConfigs')] public function testCorrectCacheControlHeadersForCacheableAction($config, $insulate) { $client = $this->createClient(['test_case' => 'Session', 'root_config' => $config]); diff --git a/Tests/Functional/SluggerLocaleAwareTest.php b/Tests/Functional/SluggerLocaleAwareTest.php index 769012461..d09f969b0 100644 --- a/Tests/Functional/SluggerLocaleAwareTest.php +++ b/Tests/Functional/SluggerLocaleAwareTest.php @@ -11,16 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Slugger\SlugConstructArgService; -/** - * @group functional - */ +#[Group('functional')] class SluggerLocaleAwareTest extends AbstractWebTestCase { - /** - * @requires extension intl - */ + #[RequiresPhpExtension('intl')] public function testLocalizedSlugger() { $kernel = static::createKernel(['test_case' => 'Slugger', 'root_config' => 'config.yml']); diff --git a/Tests/Functional/TestServiceContainerTest.php b/Tests/Functional/TestServiceContainerTest.php index fe7093081..8b8898ad8 100644 --- a/Tests/Functional/TestServiceContainerTest.php +++ b/Tests/Functional/TestServiceContainerTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Depends; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; 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; @@ -68,17 +70,13 @@ public function testSetDecoratedService() $this->assertSame($service, $container->get('decorated')->inner); } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testBootKernel() { static::bootKernel(['test_case' => 'TestServiceContainer']); } - /** - * @depends testBootKernel - */ + #[Depends('testBootKernel')] public function testKernelIsNotInitialized() { self::assertNull(self::$class); diff --git a/Tests/Functional/TranslationDebugCommandTest.php b/Tests/Functional/TranslationDebugCommandTest.php index 5e396440c..1d7e2952b 100644 --- a/Tests/Functional/TranslationDebugCommandTest.php +++ b/Tests/Functional/TranslationDebugCommandTest.php @@ -11,13 +11,12 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; +use PHPUnit\Framework\Attributes\Group; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -/** - * @group functional - */ +#[Group('functional')] class TranslationDebugCommandTest extends AbstractWebTestCase { private Application $application; diff --git a/Tests/Functional/TypeInfoTest.php b/Tests/Functional/TypeInfoTest.php index 6acdb9c81..dcda93a33 100644 --- a/Tests/Functional/TypeInfoTest.php +++ b/Tests/Functional/TypeInfoTest.php @@ -13,6 +13,7 @@ use PHPStan\PhpDocParser\Parser\PhpDocParser; use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\TypeInfo\Dummy; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\TypeInfo\Type; class TypeInfoTest extends AbstractWebTestCase @@ -28,5 +29,10 @@ public function testComponent() } $this->assertEquals(Type::int(), static::getContainer()->get('type_info.resolver')->resolve('int')); + + if (Kernel::VERSION_ID >= 70400) { + $this->assertEquals(Type::int(), static::getContainer()->get('type_info.resolver')->resolve(new \ReflectionProperty(Dummy::class, 'customAlias'))); + $this->assertEquals(Type::int(), static::getContainer()->get('type_info.resolver')->resolve('CustomAlias')); + } } } diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index 5748b61cd..fecc7fc5e 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -128,11 +128,17 @@ public function load(array $configs, ContainerBuilder $container): void { } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getNamespace(): string { return ''; } + /** + * @deprecated since Symfony 7.4, to be removed in Symfony 8.0 together with XML support. + */ public function getXsdValidationBasePath(): string|false { return false; diff --git a/Tests/Functional/app/TypeInfo/Dummy.php b/Tests/Functional/app/TypeInfo/Dummy.php index 0f517df51..b91635ad8 100644 --- a/Tests/Functional/app/TypeInfo/Dummy.php +++ b/Tests/Functional/app/TypeInfo/Dummy.php @@ -18,4 +18,9 @@ class Dummy { public string $name; + + /** + * @var CustomAlias + */ + public mixed $customAlias; } diff --git a/Tests/Functional/app/TypeInfo/config.yml b/Tests/Functional/app/TypeInfo/config.yml index 35c7bb4c4..925f10724 100644 --- a/Tests/Functional/app/TypeInfo/config.yml +++ b/Tests/Functional/app/TypeInfo/config.yml @@ -3,7 +3,9 @@ imports: framework: http_method_override: false - type_info: true + type_info: + aliases: + CustomAlias: int services: type_info.resolver.alias: diff --git a/Tests/Functional/app/templates/fragment.html.twig b/Tests/Functional/app/templates/fragment.html.twig index 6576e325a..0cac31dcf 100644 --- a/Tests/Functional/app/templates/fragment.html.twig +++ b/Tests/Functional/app/templates/fragment.html.twig @@ -1,7 +1,7 @@ -{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::inlinedAction', {'options': {'bar': bar, 'eleven': 11}})) }} +{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::inlinedAction', {options: {bar: bar, eleven: 11}})) }} -- -{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::customformatAction', {'_format': 'html'})) }} +{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::customformatAction', {_format: 'html'})) }} -- -{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::customlocaleAction', {'_locale': 'es'})) }} +{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::customlocaleAction', {_locale: 'es'})) }} -- {{ app.request.setLocale('fr') }}{{ render(controller('Symfony\\Bundle\\FrameworkBundle\\Tests\\Functional\\Bundle\\TestBundle\\Controller\\FragmentController::forwardlocaleAction')) -}} diff --git a/Tests/Kernel/ConcreteMicroKernel.php b/Tests/Kernel/ConcreteMicroKernel.php index eac061e3b..7357dc90c 100644 --- a/Tests/Kernel/ConcreteMicroKernel.php +++ b/Tests/Kernel/ConcreteMicroKernel.php @@ -24,7 +24,9 @@ class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface { - use MicroKernelTrait; + use MicroKernelTrait { + getKernelParameters as public; + } private string $cacheDir; @@ -55,6 +57,11 @@ public function getLogDir(): string return $this->cacheDir; } + public function getConfigDir(): string + { + return $this->getCacheDir().'/config'; + } + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); diff --git a/Tests/Kernel/MicroKernelTraitTest.php b/Tests/Kernel/MicroKernelTraitTest.php index 159dd21eb..4b5d6b58d 100644 --- a/Tests/Kernel/MicroKernelTraitTest.php +++ b/Tests/Kernel/MicroKernelTraitTest.php @@ -42,6 +42,7 @@ protected function tearDown(): void $this->kernel = null; $fs = new Filesystem(); $fs->remove($kernel->getCacheDir()); + $fs->remove($kernel->getProjectDir().'/config/reference.php'); } } @@ -85,7 +86,7 @@ public function testRoutingRouteLoaderTagIsAdded() public function testFlexStyle() { - $kernel = new FlexStyleMicroKernel('test', false); + $kernel = $this->kernel = new FlexStyleMicroKernel('test', false); $kernel->boot(); $request = Request::create('/'); @@ -184,4 +185,34 @@ public function testDefaultKernel() $this->assertSame('OK', $response->getContent()); } + + public function testGetKernelParameters() + { + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); + + $parameters = $kernel->getKernelParameters(); + + $this->assertSame($kernel->getConfigDir(), $parameters['.kernel.config_dir']); + $this->assertSame(['test'], $parameters['.container.known_envs']); + $this->assertSame(['Symfony\Bundle\FrameworkBundle\FrameworkBundle' => ['all' => true]], $parameters['.kernel.bundles_definition']); + } + + public function testGetKernelParametersWithBundlesFile() + { + $kernel = $this->kernel = new ConcreteMicroKernel('test', false); + + $configDir = $kernel->getConfigDir(); + mkdir($configDir, 0o777, true); + + $bundlesContent = " ['all' => true],\n 'TestBundle' => ['test' => true, 'dev' => true],\n];"; + file_put_contents($configDir.'/bundles.php', $bundlesContent); + + $parameters = $kernel->getKernelParameters(); + + $this->assertSame(['test', 'dev'], $parameters['.container.known_envs']); + $this->assertSame([ + 'Symfony\Bundle\FrameworkBundle\FrameworkBundle' => ['all' => true], + 'TestBundle' => ['test' => true, 'dev' => true], + ], $parameters['.kernel.bundles_definition']); + } } diff --git a/Tests/Routing/AttributeRouteControllerLoaderTest.php b/Tests/Routing/AttributeRouteControllerLoaderTest.php new file mode 100644 index 000000000..3f879f78e --- /dev/null +++ b/Tests/Routing/AttributeRouteControllerLoaderTest.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\FrameworkBundle\Tests\Routing; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader; +use Symfony\Bundle\FrameworkBundle\Tests\Routing\Fixtures\InvokableController; +use Symfony\Bundle\FrameworkBundle\Tests\Routing\Fixtures\MethodActionControllers; + +class AttributeRouteControllerLoaderTest extends TestCase +{ + public function testConfigureRouteSetsControllerForInvokable() + { + $loader = new AttributeRouteControllerLoader(); + $collection = $loader->load(InvokableController::class); + + $route = $collection->get('lol'); + $this->assertSame(InvokableController::class, $route->getDefault('_controller')); + } + + public function testConfigureRouteSetsControllerForMethod() + { + $loader = new AttributeRouteControllerLoader(); + $collection = $loader->load(MethodActionControllers::class); + + $put = $collection->get('put'); + $post = $collection->get('post'); + + $this->assertSame(MethodActionControllers::class.'::put', $put->getDefault('_controller')); + $this->assertSame(MethodActionControllers::class.'::post', $post->getDefault('_controller')); + } +} diff --git a/Tests/Routing/Fixtures/InvokableController.php b/Tests/Routing/Fixtures/InvokableController.php new file mode 100644 index 000000000..d9276f09a --- /dev/null +++ b/Tests/Routing/Fixtures/InvokableController.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\Tests\Routing\Fixtures; + +use Symfony\Component\Routing\Attribute\Route; + +#[Route(path: '/here', name: 'lol', methods: ['GET', 'POST'], schemes: ['https'])] +class InvokableController +{ + public function __invoke() + { + } +} diff --git a/Tests/Routing/Fixtures/MethodActionControllers.php b/Tests/Routing/Fixtures/MethodActionControllers.php new file mode 100644 index 000000000..f0adec88a --- /dev/null +++ b/Tests/Routing/Fixtures/MethodActionControllers.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\FrameworkBundle\Tests\Routing\Fixtures; + +use Symfony\Component\Routing\Attribute\Route; + +#[Route('/the/path')] +class MethodActionControllers +{ + #[Route(name: 'post', methods: ['POST'])] + public function post() + { + } + + #[Route(name: 'put', methods: ['PUT'], priority: 10)] + public function put() + { + } +} diff --git a/Tests/Routing/RouterTest.php b/Tests/Routing/RouterTest.php index d2c021563..f46522a97 100644 --- a/Tests/Routing/RouterTest.php +++ b/Tests/Routing/RouterTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Routing\Router; @@ -438,9 +439,7 @@ public function testExceptionOnNonStringParameterWithSfContainer() $router->getRouteCollection(); } - /** - * @dataProvider getNonStringValues - */ + #[DataProvider('getNonStringValues')] public function testDefaultValuesAsNonStrings($value) { $routes = new RouteCollection(); @@ -455,9 +454,7 @@ public function testDefaultValuesAsNonStrings($value) $this->assertSame($value, $route->getDefault('foo')); } - /** - * @dataProvider getNonStringValues - */ + #[DataProvider('getNonStringValues')] public function testDefaultValuesAsNonStringsWithSfContainer($value) { $routes = new RouteCollection(); @@ -525,9 +522,7 @@ public static function getNonStringValues() return [[null], [false], [true], [new \stdClass()], [['foo', 'bar']], [[[]]]]; } - /** - * @dataProvider getContainerParameterForRoute - */ + #[DataProvider('getContainerParameterForRoute')] public function testCacheValidityWithContainerParameters($parameter) { $cacheDir = tempnam(sys_get_temp_dir(), 'sf_router_'); diff --git a/Tests/Secrets/SodiumVaultTest.php b/Tests/Secrets/SodiumVaultTest.php index f91f4bced..6d050386b 100644 --- a/Tests/Secrets/SodiumVaultTest.php +++ b/Tests/Secrets/SodiumVaultTest.php @@ -11,14 +11,13 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Secrets; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\String\LazyString; -/** - * @requires extension sodium - */ +#[RequiresPhpExtension('sodium')] class SodiumVaultTest extends TestCase { private string $secretsDir; diff --git a/Tests/Test/WebTestCaseTest.php b/Tests/Test/WebTestCaseTest.php index 84f2ef0ef..a058d3628 100644 --- a/Tests/Test/WebTestCaseTest.php +++ b/Tests/Test/WebTestCaseTest.php @@ -12,13 +12,16 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Test; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\Attributes\RequiresMethod; use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\BrowserKit\History; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\HttpFoundation\Cookie as HttpFoundationCookie; use Symfony\Component\HttpFoundation\Request; @@ -190,6 +193,42 @@ public function testAssertBrowserCookieValueSame() $this->getClientTester()->assertBrowserCookieValueSame('foo', 'babar', false, '/path'); } + #[RequiresMethod(History::class, 'isFirstPage')] + public function testAssertBrowserHistoryIsOnFirstPage() + { + $this->createHistoryTester('isFirstPage', true)->assertBrowserHistoryIsOnFirstPage(); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Browser history is on the first page.'); + $this->createHistoryTester('isFirstPage', false)->assertBrowserHistoryIsOnFirstPage(); + } + + #[RequiresMethod(History::class, 'isFirstPage')] + public function testAssertBrowserHistoryIsNotOnFirstPage() + { + $this->createHistoryTester('isFirstPage', false)->assertBrowserHistoryIsNotOnFirstPage(); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Browser history is not on the first page.'); + $this->createHistoryTester('isFirstPage', true)->assertBrowserHistoryIsNotOnFirstPage(); + } + + #[RequiresMethod(History::class, 'isLastPage')] + public function testAssertBrowserHistoryIsOnLastPage() + { + $this->createHistoryTester('isLastPage', true)->assertBrowserHistoryIsOnLastPage(); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Browser history is on the last page.'); + $this->createHistoryTester('isLastPage', false)->assertBrowserHistoryIsOnLastPage(); + } + + #[RequiresMethod(History::class, 'isLastPage')] + public function testAssertBrowserHistoryIsNotOnLastPage() + { + $this->createHistoryTester('isLastPage', false)->assertBrowserHistoryIsNotOnLastPage(); + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Failed asserting that the Browser history is not on the last page.'); + $this->createHistoryTester('isLastPage', true)->assertBrowserHistoryIsNotOnLastPage(); + } + public function testAssertSelectorExists() { $this->getCrawlerTester(new Crawler('

'))->assertSelectorExists('body > h1'); @@ -386,6 +425,19 @@ private function getRequestTester(): WebTestCase return $this->getTester($client); } + private function createHistoryTester(string $method, bool $returnValue): WebTestCase + { + /** @var KernelBrowser&MockObject $client */ + $client = $this->createMock(KernelBrowser::class); + /** @var History&MockObject $history */ + $history = $this->createMock(History::class); + + $history->method($method)->willReturn($returnValue); + $client->method('getHistory')->willReturn($history); + + return $this->getTester($client); + } + private function getTester(KernelBrowser $client): WebTestCase { $tester = new class(method_exists($this, 'name') ? $this->name() : $this->getName()) extends WebTestCase { diff --git a/Tests/Translation/TranslatorTest.php b/Tests/Translation/TranslatorTest.php index e481a965e..d5f5d88eb 100644 --- a/Tests/Translation/TranslatorTest.php +++ b/Tests/Translation/TranslatorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Translation; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Translation\Translator; use Symfony\Component\Config\Resource\DirectoryResource; @@ -130,7 +131,7 @@ public function testInvalidOptions() new Translator(new Container(), new MessageFormatter(), 'en', [], ['foo' => 'bar']); } - /** @dataProvider getDebugModeAndCacheDirCombinations */ + #[DataProvider('getDebugModeAndCacheDirCombinations')] public function testResourceFilesOptionLoadsBeforeOtherAddedResources($debug, $enableCache) { $someCatalogue = $this->getCatalogue('some_locale', []); diff --git a/composer.json b/composer.json index a00bac1c3..218ab860b 100644 --- a/composer.json +++ b/composer.json @@ -19,61 +19,63 @@ "php": ">=8.2", "composer-runtime-api": ">=2.1", "ext-xml": "*", - "symfony/cache": "^6.4|^7.0", - "symfony/config": "^7.3", - "symfony/dependency-injection": "^7.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^7.3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^7.3", - "symfony/http-kernel": "^7.2", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.2|^8.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/filesystem": "^7.1", - "symfony/finder": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" }, "require-dev": { "doctrine/persistence": "^1.3|^2|^3", "dragonmantank/cron-expression": "^3.1", "seld/jsonlint": "^1.10", - "symfony/asset": "^6.4|^7.0", - "symfony/asset-mapper": "^6.4|^7.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/dotenv": "^6.4|^7.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/form": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/html-sanitizer": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/mailer": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/notifier": "^6.4|^7.0", - "symfony/object-mapper": "^v7.3.0-beta2", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/scheduler": "^6.4.4|^7.0.4", - "symfony/security-bundle": "^6.4|^7.0", - "symfony/semaphore": "^6.4|^7.0", - "symfony/serializer": "^7.2.5", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/string": "^6.4|^7.0", - "symfony/translation": "^7.3", - "symfony/twig-bundle": "^6.4|^7.0", - "symfony/type-info": "^7.1.8", - "symfony/validator": "^6.4|^7.0", - "symfony/workflow": "^7.3", - "symfony/yaml": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/json-streamer": "7.3.*", - "symfony/uid": "^6.4|^7.0", - "symfony/web-link": "^6.4|^7.0", - "symfony/webhook": "^7.2", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "twig/twig": "^3.12" }, @@ -89,12 +91,10 @@ "symfony/dom-crawler": "<6.4", "symfony/http-client": "<6.4", "symfony/form": "<6.4", - "symfony/json-streamer": ">=7.4", "symfony/lock": "<6.4", "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", + "symfony/messenger": "<7.4", "symfony/mime": "<6.4", - "symfony/object-mapper": ">=7.4", "symfony/property-info": "<6.4", "symfony/property-access": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", @@ -109,7 +109,7 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3.0-beta2" + "symfony/workflow": "<7.4" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, @@ -117,5 +117,10 @@ "/Tests/" ] }, - "minimum-stability": "dev" + "minimum-stability": "dev", + "config": { + "allow-plugins": { + "symfony/runtime": false + } + } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d00ee0f1e..90e1a751e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,11 @@ @@ -20,7 +21,7 @@ - + ./ @@ -29,5 +30,9 @@ ./Tests ./vendor - + + + + +