From 70ba6b21284d99c631b4d26e5f3c51d1f408f3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 14 May 2025 11:53:12 +0200 Subject: [PATCH 01/85] [WebLink] Add class to parse Link headers from HTTP responses --- DependencyInjection/FrameworkExtension.php | 6 ++++++ Resources/config/web_link.php | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 912282f49..38cae0ae7 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -216,6 +216,7 @@ 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\WorkflowInterface; @@ -497,6 +498,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'])) { 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'), From 79e7b61e8e79b90eece8b33012c17e9e8c2b28b3 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 1 Jun 2025 23:04:34 +0200 Subject: [PATCH 02/85] do not restrict experimental components to a single minor version --- composer.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 15a9496d1..fa4ec0074 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "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/object-mapper": "^7.3", "symfony/process": "^6.4|^7.0", "symfony/rate-limiter": "^6.4|^7.0", "symfony/scheduler": "^6.4.4|^7.0.4", @@ -70,7 +70,7 @@ "symfony/workflow": "^7.3", "symfony/yaml": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", - "symfony/json-streamer": "7.3.*", + "symfony/json-streamer": "^7.3", "symfony/uid": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0", "symfony/webhook": "^7.2", @@ -89,12 +89,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/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", From 0ecd22828dfc30cb2e7c89301aa1055e30126f93 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 30 May 2025 15:05:58 +0200 Subject: [PATCH 03/85] [DependencyInjection][FrameworkBundle] Use php-serialize to dump the container for debug/lint commands --- Command/BuildDebugContainerTrait.php | 20 ++++++---- Command/ContainerLintCommand.php | 16 +++++--- .../ContainerBuilderDebugDumpPass.php | 38 +++++++++++++++++-- 3 files changed, 59 insertions(+), 15 deletions(-) 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/ContainerLintCommand.php b/Command/ContainerLintCommand.php index e794e88c4..423b58a38 100644 --- a/Command/ContainerLintCommand.php +++ b/Command/ContainerLintCommand.php @@ -79,9 +79,10 @@ private function getContainerBuilder(): ContainerBuilder } $kernel = $this->getApplication()->getKernel(); - $kernelContainer = $kernel->getContainer(); + $container = $kernel->getContainer(); + $file = $container->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)); } @@ -93,12 +94,17 @@ private function getContainerBuilder(): 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)); + } + $parameterBag = $container->getParameterBag(); $refl = new \ReflectionProperty($parameterBag, 'resolved'); $refl->setValue($parameterBag, true); diff --git a/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php index e4023e623..b3a036c37 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,38 @@ 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($bag->all()))); + } + + $fs = new Filesystem(); + $fs->dumpFile($file, serialize($dump)); + $fs->chmod($file, 0666, umask()); + } catch (\Throwable $e) { + $container->getCompiler()->log($this, $e->getMessage()); + // ignore serialization and file-system errors + if (file_exists($file)) { + @unlink($file); + } } } } From 13dd0f884b6d96d9a490dacf0cc0d4b50c52b585 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 16:08:14 +0200 Subject: [PATCH 04/85] Allow Symfony ^8.0 --- composer.json | 94 +++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/composer.json b/composer.json index fa4ec0074..4814cc601 100644 --- a/composer.json +++ b/composer.json @@ -19,61 +19,61 @@ "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|^7.0|^8.0", + "symfony/config": "^7.3|^8.0", + "symfony/dependency-injection": "^7.2|^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/http-foundation": "^7.3|^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/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^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": "^7.3", - "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", - "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": "^6.4|^7.0|^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/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.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/workflow": "^7.3|^8.0", + "symfony/yaml": "^6.4|^7.0|^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" }, From 59fddfe981668f6cbb7edba751130f065adcd899 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Jun 2025 10:47:29 +0200 Subject: [PATCH 05/85] Tweak return type declarations and related CI checks --- Tests/Kernel/MicroKernelTraitTest.php | 25 ----------------- Tests/Kernel/MinimalKernel.php | 39 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 Tests/Kernel/MinimalKernel.php diff --git a/Tests/Kernel/MicroKernelTraitTest.php b/Tests/Kernel/MicroKernelTraitTest.php index 5c7161124..159dd21eb 100644 --- a/Tests/Kernel/MicroKernelTraitTest.php +++ b/Tests/Kernel/MicroKernelTraitTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -186,27 +185,3 @@ public function testDefaultKernel() $this->assertSame('OK', $response->getContent()); } } - -abstract class MinimalKernel extends Kernel -{ - use MicroKernelTrait; - - private string $cacheDir; - - public function __construct(string $cacheDir) - { - parent::__construct('test', false); - - $this->cacheDir = sys_get_temp_dir().'/'.$cacheDir; - } - - public function getCacheDir(): string - { - return $this->cacheDir; - } - - public function getLogDir(): string - { - return $this->cacheDir; - } -} diff --git a/Tests/Kernel/MinimalKernel.php b/Tests/Kernel/MinimalKernel.php new file mode 100644 index 000000000..df2c97e6a --- /dev/null +++ b/Tests/Kernel/MinimalKernel.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel; + +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Component\HttpKernel\Kernel; + +abstract class MinimalKernel extends Kernel +{ + use MicroKernelTrait; + + private string $cacheDir; + + public function __construct(string $cacheDir) + { + parent::__construct('test', false); + + $this->cacheDir = sys_get_temp_dir().'/'.$cacheDir; + } + + public function getCacheDir(): string + { + return $this->cacheDir; + } + + public function getLogDir(): string + { + return $this->cacheDir; + } +} From ccdd87db3c227aba9e1e3598777bb7438af00fdf Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 9 Jun 2025 17:40:54 +0200 Subject: [PATCH 06/85] [Console] Simplify using invokable commands when the component is used standalone --- CHANGELOG.md | 5 +++++ Console/Application.php | 22 +++++++++++++++++-- .../Command/AboutCommand/AboutCommandTest.php | 2 +- Tests/Command/CachePoolClearCommandTest.php | 2 +- Tests/Command/CachePoolDeleteCommandTest.php | 4 ++-- Tests/Command/CachePruneCommandTest.php | 2 +- Tests/Command/RouterMatchCommandTest.php | 4 ++-- Tests/Command/TranslationDebugCommandTest.php | 2 +- ...ranslationExtractCommandCompletionTest.php | 2 +- .../Command/TranslationExtractCommandTest.php | 2 +- Tests/Command/WorkflowDumpCommandTest.php | 7 +++++- Tests/Command/XliffLintCommandTest.php | 7 +++++- Tests/Command/YamlLintCommandTest.php | 7 +++++- Tests/Console/ApplicationTest.php | 2 +- Tests/Functional/BundlePathsTest.php | 2 +- .../Functional/CachePoolClearCommandTest.php | 2 +- Tests/Functional/CachePoolListCommandTest.php | 2 +- Tests/Functional/ConfigDebugCommandTest.php | 2 +- .../ConfigDumpReferenceCommandTest.php | 2 +- .../Functional/DebugAutowiringCommandTest.php | 2 +- 20 files changed, 60 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce62c9cdf..203644c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Deprecate `Symfony\Bundle\FrameworkBundle\Console\Application::add()` in favor of `Symfony\Bundle\FrameworkBundle\Console\Application::addCommand()` + 7.3 --- 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/Tests/Command/AboutCommand/AboutCommandTest.php b/Tests/Command/AboutCommand/AboutCommandTest.php index bcf3c7fe0..ee3904be3 100644 --- a/Tests/Command/AboutCommand/AboutCommandTest.php +++ b/Tests/Command/AboutCommand/AboutCommandTest.php @@ -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..c98d7ed92 100644 --- a/Tests/Command/CachePoolClearCommandTest.php +++ b/Tests/Command/CachePoolClearCommandTest.php @@ -36,7 +36,7 @@ protected function setUp(): void 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..b4c11d4db 100644 --- a/Tests/Command/CachePoolDeleteCommandTest.php +++ b/Tests/Command/CachePoolDeleteCommandTest.php @@ -90,7 +90,7 @@ public function testCommandDeleteFailed() 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 +125,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/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/TranslationDebugCommandTest.php b/Tests/Command/TranslationDebugCommandTest.php index c6c91a857..1b114ad49 100644 --- a/Tests/Command/TranslationDebugCommandTest.php +++ b/Tests/Command/TranslationDebugCommandTest.php @@ -223,7 +223,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'); } diff --git a/Tests/Command/TranslationExtractCommandCompletionTest.php b/Tests/Command/TranslationExtractCommandCompletionTest.php index 6d2f22d96..a47b0913f 100644 --- a/Tests/Command/TranslationExtractCommandCompletionTest.php +++ b/Tests/Command/TranslationExtractCommandCompletionTest.php @@ -132,7 +132,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..22927d210 100644 --- a/Tests/Command/TranslationExtractCommandTest.php +++ b/Tests/Command/TranslationExtractCommandTest.php @@ -304,7 +304,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 index 284e97623..34009756a 100644 --- a/Tests/Command/WorkflowDumpCommandTest.php +++ b/Tests/Command/WorkflowDumpCommandTest.php @@ -25,7 +25,12 @@ class WorkflowDumpCommandTest extends TestCase public function testComplete(array $input, array $expectedSuggestions) { $application = new Application(); - $application->add(new WorkflowDumpCommand(new ServiceLocator([]))); + $command = new WorkflowDumpCommand(new ServiceLocator([])); + if (method_exists($application, 'addCommand')) { + $application->addCommand($command); + } else { + $application->add($command); + } $tester = new CommandCompletionTester($application->find('workflow:dump')); $suggestions = $tester->complete($input, 2); diff --git a/Tests/Command/XliffLintCommandTest.php b/Tests/Command/XliffLintCommandTest.php index d5495ada9..ed96fbb00 100644 --- a/Tests/Command/XliffLintCommandTest.php +++ b/Tests/Command/XliffLintCommandTest.php @@ -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..30a73015b 100644 --- a/Tests/Command/YamlLintCommandTest.php +++ b/Tests/Command/YamlLintCommandTest.php @@ -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/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..a2966b5a2 100644 --- a/Tests/Functional/CachePoolClearCommandTest.php +++ b/Tests/Functional/CachePoolClearCommandTest.php @@ -146,7 +146,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..eec484026 100644 --- a/Tests/Functional/CachePoolListCommandTest.php +++ b/Tests/Functional/CachePoolListCommandTest.php @@ -46,7 +46,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/ConfigDebugCommandTest.php b/Tests/Functional/ConfigDebugCommandTest.php index bd1539636..1819e7f4e 100644 --- a/Tests/Functional/ConfigDebugCommandTest.php +++ b/Tests/Functional/ConfigDebugCommandTest.php @@ -241,7 +241,7 @@ public function testComplete(bool $debug, array $input, array $expectedSuggestio { $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..a16d8e046 100644 --- a/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -132,7 +132,7 @@ public function testComplete(bool $debug, array $input, array $expectedSuggestio { $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/DebugAutowiringCommandTest.php b/Tests/Functional/DebugAutowiringCommandTest.php index ca11e3fae..b43a12ed6 100644 --- a/Tests/Functional/DebugAutowiringCommandTest.php +++ b/Tests/Functional/DebugAutowiringCommandTest.php @@ -122,7 +122,7 @@ public function testNotConfusedByClassAliases() 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); From 2075113566a543cfc698b4f48332d4c45ac3aa99 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Wed, 11 Jun 2025 19:08:03 -0300 Subject: [PATCH 07/85] [Mailer] Add new `assertEmailAddressNotContains` method --- CHANGELOG.md | 1 + Test/MailerAssertionsTrait.php | 5 +++++ Tests/Functional/MailerTest.php | 3 +++ 3 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 203644c01..b18639e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Deprecate `Symfony\Bundle\FrameworkBundle\Console\Application::add()` in favor of `Symfony\Bundle\FrameworkBundle\Console\Application::addCommand()` + * Add `assertEmailAddressNotContains()` to the `MailerAssertionsTrait` 7.3 --- 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/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'); } } From 6f1e900eb37c90e6ebc84893f1a6bd6b0e17ce31 Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Mon, 9 Jun 2025 08:20:00 -0400 Subject: [PATCH 08/85] Improve-callable-typing --- Console/Descriptor/TextDescriptor.php | 3 +++ Console/Descriptor/XmlDescriptor.php | 3 +++ Routing/Router.php | 3 +++ 3 files changed, 9 insertions(+) diff --git a/Console/Descriptor/TextDescriptor.php b/Console/Descriptor/TextDescriptor.php index 12b345411..a5b31b186 100644 --- a/Console/Descriptor/TextDescriptor.php +++ b/Console/Descriptor/TextDescriptor.php @@ -576,6 +576,9 @@ private function formatRouterConfig(array $config): string return trim($configAsString); } + /** + * @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..6a25ae3a3 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'); 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; /** From 2967178b1b9a26650f78a8e66871b2b68f38d64b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Jun 2025 23:13:45 +0200 Subject: [PATCH 09/85] [FrameworkBundle] Allow to un-verbose all the method in `BrowserKitAssertionsTrait` --- Test/BrowserKitAssertionsTrait.php | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Test/BrowserKitAssertionsTrait.php b/Test/BrowserKitAssertionsTrait.php index 1b7437b77..7d49aa61d 100644 --- a/Test/BrowserKitAssertionsTrait.php +++ b/Test/BrowserKitAssertionsTrait.php @@ -28,24 +28,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::$defaultVerboseMode = $verbose; + } + + public static function assertResponseIsSuccessful(string $message = '', ?bool $verbose = null): void { - self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful($verbose), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful($verbose ?? self::$defaultVerboseMode), $message); } - public static function assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true): void + public static function assertResponseStatusCodeSame(int $expectedCode, string $message = '', ?bool $verbose = null): void { - self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose), $message); + self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode, $verbose ?? self::$defaultVerboseMode), $message); } - public static function assertResponseFormatSame(?string $expectedFormat, string $message = ''): void + public static function assertResponseFormatSame(?string $expectedFormat, string $message = '', ?bool $verbose = null): void { - self::assertThatForResponse(new ResponseConstraint\ResponseFormatSame(self::getRequest(), $expectedFormat), $message); + 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 = true): void + public static function assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', ?bool $verbose = null): void { - $constraint = new ResponseConstraint\ResponseIsRedirected($verbose); + $constraint = new ResponseConstraint\ResponseIsRedirected($verbose ?? self::$defaultVerboseMode); if ($expectedLocation) { if (class_exists(ResponseConstraint\ResponseHeaderLocationSame::class)) { $locationConstraint = new ResponseConstraint\ResponseHeaderLocationSame(self::getRequest(), $expectedLocation); @@ -100,9 +107,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 From fc538543f669b1878e924dfc1c22d634162a369c Mon Sep 17 00:00:00 2001 From: Valmonzo Date: Mon, 23 Jun 2025 12:02:09 +0200 Subject: [PATCH 10/85] [FrameworkBundle] Allow using their name without added suffix when using #[Target] for custom services --- CHANGELOG.md | 1 + DependencyInjection/FrameworkExtension.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b18639e4c..1256fbf38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.4 --- + * 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` diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index c4ff440cc..6f0e1d492 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1426,6 +1426,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co ->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); } } @@ -2247,6 +2248,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont $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); } } } @@ -2282,6 +2284,7 @@ private function registerSemaphoreConfiguration(array $config, ContainerBuilder $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); } } } @@ -3307,7 +3310,8 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde if (interface_exists(RateLimiterFactoryInterface::class)) { $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); - $factoryAlias->setDeprecated('symfony/dependency-injection', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name); + $factoryAlias->setDeprecated('symfony/framework-bundle', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); } } From 1d8af873939cc96060430cd731adf5e5dfe62152 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 18 Jun 2025 15:29:22 +0200 Subject: [PATCH 11/85] [FrameworkBundle] Add `ControllerHelper`; the helpers from AbstractController as a standalone service --- CHANGELOG.md | 1 + Controller/AbstractController.php | 24 +- Controller/ControllerHelper.php | 473 ++++++++++++++++++++ Resources/config/web.php | 6 + Tests/Controller/AbstractControllerTest.php | 2 +- Tests/Controller/ControllerHelperTest.php | 64 +++ 6 files changed, 557 insertions(+), 13 deletions(-) create mode 100644 Controller/ControllerHelper.php create mode 100644 Tests/Controller/ControllerHelperTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1256fbf38..de31d3d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.4 --- + * 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` 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/Resources/config/web.php b/Resources/config/web.php index a4e975dac..320bda08f 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; @@ -146,5 +147,10 @@ ->set('controller.cache_attribute_listener', CacheAttributeListener::class) ->tag('kernel.event_subscriber') + ->set('controller.helper', ControllerHelper::class) + ->tag('container.service_subscriber') + + ->alias(ControllerHelper::class, 'controller.helper') + ; }; diff --git a/Tests/Controller/AbstractControllerTest.php b/Tests/Controller/AbstractControllerTest.php index 5f5fc5ca5..91c30d1d9 100644 --- a/Tests/Controller/AbstractControllerTest.php +++ b/Tests/Controller/AbstractControllerTest.php @@ -101,7 +101,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'); } diff --git a/Tests/Controller/ControllerHelperTest.php b/Tests/Controller/ControllerHelperTest.php new file mode 100644 index 000000000..e12c1ed7f --- /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'); + } +} From fc250ed3fb29fa989b8dffde98dde43031d8c340 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 26 Jun 2025 12:09:09 +0200 Subject: [PATCH 12/85] [DependencyInjection] Add argument `$target` to `ContainerBuilder::registerAliasForArgument()` --- Command/DebugAutowiringCommand.php | 2 +- DependencyInjection/FrameworkExtension.php | 24 ++++++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Command/DebugAutowiringCommand.php b/Command/DebugAutowiringCommand.php index e159c5a39..ddebb35c6 100644 --- a/Command/DebugAutowiringCommand.php +++ b/Command/DebugAutowiringCommand.php @@ -137,7 +137,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; } diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 8dca1fa4b..a427a0061 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1190,8 +1190,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']) { @@ -1426,8 +1425,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); + $container->registerAliasForArgument('assets._package_'.$name, PackageInterface::class, $name.'.package', $name); } } @@ -2248,8 +2246,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); + $container->registerAliasForArgument('lock.'.$resourceName.'.factory', LockFactory::class, $resourceName.'.lock.factory', $resourceName); } } } @@ -2284,8 +2281,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); + $container->registerAliasForArgument('semaphore.'.$resourceName.'.factory', SemaphoreFactory::class, $resourceName.'.semaphore.factory', $resourceName); } } } @@ -3310,13 +3306,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.'); } } @@ -3341,7 +3335,7 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde ))) ; - $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter', $name); } } From fc5abf6f617be39b59b6da035dd388a2dca3b07c Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 27 Jun 2025 23:42:21 +0200 Subject: [PATCH 13/85] Fix typos in documentation and code comments --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de31d3d7b..76b3cb947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -706,7 +706,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 From daee95f2ef3a6da589c7a9be8a6d9fb2dc2bba99 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Sat, 28 Jun 2025 18:24:36 -0300 Subject: [PATCH 14/85] [FrameworkBundle] fix `lint:container` command --- Command/ContainerLintCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/ContainerLintCommand.php b/Command/ContainerLintCommand.php index 423b58a38..c201a9728 100644 --- a/Command/ContainerLintCommand.php +++ b/Command/ContainerLintCommand.php @@ -80,7 +80,7 @@ private function getContainerBuilder(): ContainerBuilder $kernel = $this->getApplication()->getKernel(); $container = $kernel->getContainer(); - $file = $container->isDebug() ? $container->getParameter('debug.container.dump') : false; + $file = $kernel->isDebug() ? $container->getParameter('debug.container.dump') : false; if (!$file || !(new ConfigCache($file, true))->isFresh()) { if (!$kernel instanceof Kernel) { From c8c2b51f260640e6f9414e7dcf434b9f2836296b Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Sun, 29 Jun 2025 15:07:21 -0300 Subject: [PATCH 15/85] [FrameworkBundle] Minor remove unused `Container` use statement in `ContainerLintCommand` --- Command/ContainerLintCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Command/ContainerLintCommand.php b/Command/ContainerLintCommand.php index c201a9728..1b77eb6dc 100644 --- a/Command/ContainerLintCommand.php +++ b/Command/ContainerLintCommand.php @@ -24,7 +24,6 @@ use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\ResolveFactoryClassPass; -use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; From 3b0b4dc508e1a3e1e01df0a5a8eb394422f3eb9e Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Mon, 30 Jun 2025 00:17:16 -0300 Subject: [PATCH 16/85] [BrowserKit] Add PHPUnit constraints: `BrowserHistoryIsOnFirstPage` and `BrowserHistoryIsOnLastPage` --- Test/BrowserKitAssertionsTrait.php | 33 +++++++++++++++++ Tests/Test/WebTestCaseTest.php | 59 ++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/Test/BrowserKitAssertionsTrait.php b/Test/BrowserKitAssertionsTrait.php index 7d49aa61d..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; @@ -122,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/Tests/Test/WebTestCaseTest.php b/Tests/Test/WebTestCaseTest.php index 84f2ef0ef..fd65b18d8 100644 --- a/Tests/Test/WebTestCaseTest.php +++ b/Tests/Test/WebTestCaseTest.php @@ -13,12 +13,14 @@ use PHPUnit\Framework\AssertionFailedError; 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 +192,50 @@ public function testAssertBrowserCookieValueSame() $this->getClientTester()->assertBrowserCookieValueSame('foo', 'babar', false, '/path'); } + /** + * @requires function \Symfony\Component\BrowserKit\History::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(); + } + + /** + * @requires function \Symfony\Component\BrowserKit\History::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(); + } + + /** + * @requires function \Symfony\Component\BrowserKit\History::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(); + } + + /** + * @requires function \Symfony\Component\BrowserKit\History::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 +432,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 { From d90ae1bf5b46f915e38fae80e747ceb7590321ab Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 5 Jul 2025 15:43:06 +0200 Subject: [PATCH 17/85] chore: PHP CS Fixer fixes --- DependencyInjection/FrameworkExtension.php | 2 +- Tests/DependencyInjection/PhpFrameworkExtensionTest.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index d3cefbb28..d024b7a4e 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1159,7 +1159,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $workflow['definition_validators'][] = match ($workflow['type']) { 'state_machine' => Workflow\Validator\StateMachineValidator::class, 'workflow' => Workflow\Validator\WorkflowValidator::class, - default => throw new \LogicException(\sprintf('Invalid workflow type "%s".', $workflow['type'])), + default => throw new \LogicException(\sprintf('Invalid workflow type "%s".', $workflow['type'])), }; // Create Workflow diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 65826f698..c4f67c2f1 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -440,10 +440,10 @@ public function testValidatorEmailValidationMode(string $mode) $this->createContainerFromClosure(function (ContainerBuilder $container) use ($mode) { $container->loadFromExtension('framework', [ - 'annotations' => false, - 'http_method_override' => false, - 'handle_all_throwables' => true, - 'php_errors' => ['log' => true], + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], 'validation' => [ 'email_validation_mode' => $mode, ], From 7464e872661db10229cebd3aaf17f3c9839cbb3a Mon Sep 17 00:00:00 2001 From: Andrii Date: Sat, 28 Jun 2025 02:44:33 +0200 Subject: [PATCH 18/85] [HttpKernel] Avoid memory leaks cache attribute, profiler listener --- Resources/config/profiling.php | 1 + Resources/config/web.php | 1 + 2 files changed, 2 insertions(+) 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/web.php b/Resources/config/web.php index 320bda08f..29e128715 100644 --- a/Resources/config/web.php +++ b/Resources/config/web.php @@ -146,6 +146,7 @@ ->set('controller.cache_attribute_listener', CacheAttributeListener::class) ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => '?reset']) ->set('controller.helper', ControllerHelper::class) ->tag('container.service_subscriber') From 5b22ac9de8b70b33bd123dcb5c23774346abc66e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 8 Jul 2025 11:08:29 +0200 Subject: [PATCH 19/85] Various CS fixes --- Command/DebugAutowiringCommand.php | 1 - DependencyInjection/FrameworkExtension.php | 5 ++--- Tests/Controller/ControllerHelperTest.php | 10 +++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Command/DebugAutowiringCommand.php b/Command/DebugAutowiringCommand.php index ddebb35c6..85f546c2f 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; /** diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index c8d2624f4..a9cabf4f1 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -61,7 +61,6 @@ use Symfony\Component\DependencyInjection\Alias; 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; @@ -3323,13 +3322,13 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter.', $name)); } - if (\array_diff($limiterConfig['limiters'], $limiters)) { + if (array_diff($limiterConfig['limiters'], $limiters)) { throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter to be configured.', $name)); } $container->register($limiterId = 'limiter.'.$name, CompoundRateLimiterFactory::class) ->addTag('rate_limiter', ['name' => $name]) - ->addArgument(new IteratorArgument(\array_map( + ->addArgument(new IteratorArgument(array_map( static fn (string $name) => new Reference('limiter.'.$name), $limiterConfig['limiters'] ))) diff --git a/Tests/Controller/ControllerHelperTest.php b/Tests/Controller/ControllerHelperTest.php index e12c1ed7f..cb35c4757 100644 --- a/Tests/Controller/ControllerHelperTest.php +++ b/Tests/Controller/ControllerHelperTest.php @@ -19,7 +19,7 @@ class ControllerHelperTest extends AbstractControllerTest { protected function createController() { - return new class() extends ControllerHelper { + return new class extends ControllerHelper { public function __construct() { } @@ -36,26 +36,26 @@ 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())); + $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)), + 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); + $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('', \array_slice($abstractSrc, $m->getStartLine() - 1, $m->getEndLine() - $m->getStartLine() + 1)); } $code = implode("\n", $code); From cfd70c4994657db428cd52a5bfba7f07b02fe8fe Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Tue, 8 Jul 2025 19:39:48 +0200 Subject: [PATCH 20/85] Leverage get_error_handler() --- FrameworkBundle.php | 3 +-- composer.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 300fe22fb..34e8b3ae7 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -103,8 +103,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)]; } diff --git a/composer.json b/composer.json index 1ff4abd0f..ff3f8bd2e 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "symfony/http-foundation": "^7.3|^8.0", "symfony/http-kernel": "^7.2|^8.0", "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php85": "^1.32", "symfony/filesystem": "^7.1|^8.0", "symfony/finder": "^6.4|^7.0|^8.0", "symfony/routing": "^6.4|^7.0|^8.0" From 31c77b4c7c12d5446859a7a00b3c1316177ae997 Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Sat, 12 Jul 2025 15:55:19 +0200 Subject: [PATCH 21/85] optimize `in_array` calls --- Command/CacheClearCommand.php | 2 +- DependencyInjection/Configuration.php | 2 +- DependencyInjection/FrameworkExtension.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Command/CacheClearCommand.php b/Command/CacheClearCommand.php index 0e48ead59..01ddedde3 100644 --- a/Command/CacheClearCommand.php +++ b/Command/CacheClearCommand.php @@ -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/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index d042d44b5..1790db359 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2563,7 +2563,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() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index a9cabf4f1..e055f5f8b 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2053,7 +2053,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $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; }; @@ -2935,7 +2935,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]); From b0c570d08de0d5a67efb7064968aac859b3075e7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 24 Jul 2025 14:45:41 +0200 Subject: [PATCH 22/85] Fix typos --- Tests/Functional/ContainerDebugCommandTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index d21d4d113..9b70c7bb3 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -214,10 +214,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 +233,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() From e5fcc444ed047dd2de86f78d5a40513add19a860 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 16 Jun 2025 16:37:14 +0200 Subject: [PATCH 23/85] [Validator] Add `min` and `max` in both error messages of `LengthValidator` --- Tests/Functional/ApiAttributesTest.php | 9 +++++++++ composer.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Tests/Functional/ApiAttributesTest.php b/Tests/Functional/ApiAttributesTest.php index 0dcfeaeba..8831fcd64 100644 --- a/Tests/Functional/ApiAttributesTest.php +++ b/Tests/Functional/ApiAttributesTest.php @@ -405,6 +405,7 @@ public static function mapRequestPayloadProvider(): iterable "parameters": { "{{ value }}": "\"\"", "{{ limit }}": "10", + "{{ min }}": "10", "{{ value_length }}": "0" }, "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45" @@ -439,6 +440,7 @@ public static function mapRequestPayloadProvider(): iterable "H" 10 + 10 1 urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45 @@ -476,6 +478,7 @@ public static function mapRequestPayloadProvider(): iterable "parameters": { "{{ value }}": "\"\"", "{{ limit }}": "10", + "{{ min }}": "10", "{{ value_length }}": "0" }, "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45" @@ -646,6 +649,7 @@ public static function mapRequestPayloadProvider(): iterable "parameters": { "{{ value }}": "\"\"", "{{ limit }}": "10", + "{{ min }}": "10", "{{ value_length }}": "0" }, "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45" @@ -680,6 +684,7 @@ public static function mapRequestPayloadProvider(): iterable "H" 10 + 10 1 urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45 @@ -717,6 +722,7 @@ public static function mapRequestPayloadProvider(): iterable "parameters": { "{{ value }}": "\"\"", "{{ limit }}": "10", + "{{ min }}": "10", "{{ value_length }}": "0" }, "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45" @@ -892,6 +898,7 @@ public static function mapRequestPayloadProvider(): iterable "parameters": { "{{ value }}": "\"\"", "{{ limit }}": "10", + "{{ min }}": "10", "{{ value_length }}": "0" }, "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45" @@ -926,6 +933,7 @@ public static function mapRequestPayloadProvider(): iterable "H" 10 + 10 1 urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45 @@ -963,6 +971,7 @@ public static function mapRequestPayloadProvider(): iterable "parameters": { "{{ value }}": "\"\"", "{{ limit }}": "10", + "{{ min }}": "10", "{{ value_length }}": "0" }, "type": "urn:uuid:9ff3fdc4-b214-49db-8718-39c315e33d45" diff --git a/composer.json b/composer.json index ff3f8bd2e..0c3dfd4c4 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "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": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", "symfony/workflow": "^7.3|^8.0", "symfony/yaml": "^6.4|^7.0|^8.0", "symfony/property-info": "^6.4|^7.0|^8.0", From 159c766c65aacccbe55c8d5639f783a84ff0d335 Mon Sep 17 00:00:00 2001 From: Gary PEGEOT Date: Wed, 23 Jul 2025 11:32:32 +0200 Subject: [PATCH 24/85] [DependencyInjection] Update `ResolveClassPass` to check class existence --- .../TestServiceContainerRefPassesTest.php | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) 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()); } } From 335fabe41ff7c2746d78be9b2df2c56faac36d28 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 9 Oct 2024 11:06:51 +0200 Subject: [PATCH 25/85] run tests using PHPUnit 11.5 --- .../Descriptor/AbstractDescriptorTestCase.php | 20 +++++++++---------- Tests/Functional/PropertyInfoTest.php | 7 ++++--- composer.json | 8 +++++++- phpunit.xml.dist | 11 +++++++--- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index eb18fbcc7..d09fa8e4f 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; @@ -185,12 +188,11 @@ 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. */ + #[IgnoreDeprecations] + #[Group('legacy')] + #[DataProvider('getDescribeContainerParameterTestData')] public function testDescribeContainerParameter($parameter, $expectedDescription, array $options) { $this->assertDescription($expectedDescription, $parameter, $options); @@ -235,11 +237,9 @@ public static function getDescribeCallableTestData(): array return static::getDescriptionTestData(ObjectsProvider::getCallables()); } - /** - * @group legacy - * - * @dataProvider getDescribeDeprecatedCallableTestData - */ + #[IgnoreDeprecations] + #[Group('legacy')] + #[DataProvider('getDescribeDeprecatedCallableTestData')] public function testDescribeDeprecatedCallable($callable, $expectedDescription) { $this->assertDescription($expectedDescription, $callable); 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/composer.json b/composer.json index 0c3dfd4c4..3f7a22462 100644 --- a/composer.json +++ b/composer.json @@ -58,6 +58,7 @@ "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", @@ -116,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 - + + + + + From cf857df724f6f83bcd6e3da8349e0a6e7aa1bbfc Mon Sep 17 00:00:00 2001 From: matlec Date: Sat, 2 Aug 2025 18:55:44 +0200 Subject: [PATCH 26/85] [FrameworkBundle] Escape parameters when serializing a ContainerBuilder --- .../Compiler/ContainerBuilderDebugDumpPass.php | 16 +++++++++++++++- Tests/Functional/ContainerLintCommandTest.php | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php index b3a036c37..ff9020796 100644 --- a/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php +++ b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php @@ -54,7 +54,7 @@ public function process(ContainerBuilder $container): void if (($bag = $container->getParameterBag()) instanceof EnvPlaceholderParameterBag) { (new ResolveEnvPlaceholdersPass(null))->process($dump); - $dump->__construct(new EnvPlaceholderParameterBag($container->resolveEnvPlaceholders($bag->all()))); + $dump->__construct(new EnvPlaceholderParameterBag($container->resolveEnvPlaceholders($this->escapeParameters($bag->all())))); } $fs = new Filesystem(); @@ -68,4 +68,18 @@ public function process(ContainerBuilder $container): void } } } + + 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/Tests/Functional/ContainerLintCommandTest.php b/Tests/Functional/ContainerLintCommandTest.php index f0b6b4bd5..106f6b776 100644 --- a/Tests/Functional/ContainerLintCommandTest.php +++ b/Tests/Functional/ContainerLintCommandTest.php @@ -44,6 +44,7 @@ public static function containerLintProvider(): array { return [ ['escaped_percent.yml', false, 0, 'The container was linted successfully'], + ['escaped_percent.yml', true, 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"'], ]; From 515d9936bd6a97ffba71775471c7aab83b793d04 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 3 Aug 2025 21:21:25 +0200 Subject: [PATCH 27/85] fix low deps tests --- Tests/Functional/ContainerLintCommandTest.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Tests/Functional/ContainerLintCommandTest.php b/Tests/Functional/ContainerLintCommandTest.php index 106f6b776..a4af3649a 100644 --- a/Tests/Functional/ContainerLintCommandTest.php +++ b/Tests/Functional/ContainerLintCommandTest.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\Argument\ArgumentTrait; /** * @group functional @@ -40,14 +41,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'], - ['escaped_percent.yml', true, 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 From 7095ef54a5a257e943025dbec5e3bb3506e2f388 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 31 Jul 2025 14:36:46 +0200 Subject: [PATCH 28/85] replace PHPUnit annotations with attributes --- .../CacheWarmer/SerializerCacheWarmerTest.php | 13 +-- Tests/Command/CachePoolClearCommandTest.php | 5 +- Tests/Command/CachePoolDeleteCommandTest.php | 5 +- .../EventDispatcherDebugCommandTest.php | 5 +- Tests/Command/SecretsListCommandTest.php | 5 +- Tests/Command/SecretsRemoveCommandTest.php | 5 +- Tests/Command/SecretsRevealCommandTest.php | 9 +- Tests/Command/SecretsSetCommandTest.php | 5 +- Tests/Command/TranslationDebugCommandTest.php | 5 +- ...ranslationExtractCommandCompletionTest.php | 5 +- .../Command/TranslationExtractCommandTest.php | 5 +- Tests/Command/WorkflowDumpCommandTest.php | 5 +- .../Descriptor/AbstractDescriptorTestCase.php | 34 +++--- .../Console/Descriptor/TextDescriptorTest.php | 3 +- Tests/Controller/AbstractControllerTest.php | 10 +- Tests/Controller/RedirectControllerTest.php | 13 +-- .../Compiler/ProfilerPassTest.php | 5 +- .../DependencyInjection/ConfigurationTest.php | 25 ++--- .../FrameworkExtensionTestCase.php | 13 +-- .../PhpFrameworkExtensionTest.php | 9 +- Tests/Fixtures/Descriptor/route_1_link.txt | 2 +- Tests/Fixtures/Descriptor/route_2_link.txt | 2 +- .../AbstractAttributeRoutingTestCase.php | 5 +- Tests/Functional/ApiAttributesTest.php | 11 +- .../Functional/CachePoolClearCommandTest.php | 5 +- Tests/Functional/CachePoolListCommandTest.php | 5 +- Tests/Functional/CachePoolsTest.php | 21 ++-- Tests/Functional/ConfigDebugCommandTest.php | 101 ++++++------------ .../ConfigDumpReferenceCommandTest.php | 47 +++----- .../Functional/ContainerDebugCommandTest.php | 14 +-- Tests/Functional/ContainerLintCommandTest.php | 10 +- .../Functional/DebugAutowiringCommandTest.php | 10 +- Tests/Functional/FragmentTest.php | 6 +- Tests/Functional/NotificationTest.php | 7 +- Tests/Functional/ProfilerTest.php | 10 +- Tests/Functional/RouterDebugCommandTest.php | 21 ++-- .../RoutingConditionServiceTest.php | 6 +- Tests/Functional/SecurityTest.php | 5 +- Tests/Functional/SessionTest.php | 15 ++- Tests/Functional/SluggerLocaleAwareTest.php | 10 +- Tests/Functional/TestServiceContainerTest.php | 10 +- .../TranslationDebugCommandTest.php | 5 +- Tests/Routing/RouterTest.php | 13 +-- Tests/Secrets/SodiumVaultTest.php | 5 +- Tests/Test/WebTestCaseTest.php | 17 +-- Tests/Translation/TranslatorTest.php | 3 +- 46 files changed, 208 insertions(+), 342 deletions(-) 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/CachePoolClearCommandTest.php b/Tests/Command/CachePoolClearCommandTest.php index c98d7ed92..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,9 +31,7 @@ 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()); diff --git a/Tests/Command/CachePoolDeleteCommandTest.php b/Tests/Command/CachePoolDeleteCommandTest.php index b4c11d4db..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,9 +85,7 @@ 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()); 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/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 1b114ad49..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; @@ -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 a47b0913f..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']]); diff --git a/Tests/Command/TranslationExtractCommandTest.php b/Tests/Command/TranslationExtractCommandTest.php index 22927d210..89361e825 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; @@ -177,9 +178,7 @@ public function testFilterDuplicateTransPaths() $this->assertEquals($expectedPaths, $filteredTransPaths); } - /** - * @dataProvider removeNoFillProvider - */ + #[DataProvider('removeNoFillProvider')] public function testRemoveNoFillTranslationsMethod($noFillCounter, $messages) { // Preparing mock diff --git a/Tests/Command/WorkflowDumpCommandTest.php b/Tests/Command/WorkflowDumpCommandTest.php index 34009756a..d7d17a923 100644 --- a/Tests/Command/WorkflowDumpCommandTest.php +++ b/Tests/Command/WorkflowDumpCommandTest.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\WorkflowDumpCommand; use Symfony\Component\Console\Application; @@ -19,9 +20,7 @@ class WorkflowDumpCommandTest extends TestCase { - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $application = new Application(); diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index d09fa8e4f..f52a1d8de 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -42,7 +42,7 @@ protected function tearDown(): void putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } - /** @dataProvider getDescribeRouteCollectionTestData */ + #[DataProvider('getDescribeRouteCollectionTestData')] public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription) { $this->assertDescription($expectedDescription, $routes); @@ -53,7 +53,7 @@ public static function getDescribeRouteCollectionTestData(): array return static::getDescriptionTestData(ObjectsProvider::getRouteCollections()); } - /** @dataProvider getDescribeRouteCollectionWithHttpMethodFilterTestData */ + #[DataProvider('getDescribeRouteCollectionWithHttpMethodFilterTestData')] public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription) { $this->assertDescription($expectedDescription, $routes, ['method' => $httpMethod]); @@ -68,7 +68,7 @@ public static function getDescribeRouteCollectionWithHttpMethodFilterTestData(): } } - /** @dataProvider getDescribeRouteTestData */ + #[DataProvider('getDescribeRouteTestData')] public function testDescribeRoute(Route $route, $expectedDescription) { $this->assertDescription($expectedDescription, $route); @@ -79,7 +79,7 @@ public static function getDescribeRouteTestData(): array return static::getDescriptionTestData(ObjectsProvider::getRoutes()); } - /** @dataProvider getDescribeContainerParametersTestData */ + #[DataProvider('getDescribeContainerParametersTestData')] public function testDescribeContainerParameters(ParameterBag $parameters, $expectedDescription) { $this->assertDescription($expectedDescription, $parameters); @@ -90,7 +90,7 @@ public static function getDescribeContainerParametersTestData(): array return static::getDescriptionTestData(ObjectsProvider::getContainerParameters()); } - /** @dataProvider getDescribeContainerBuilderTestData */ + #[DataProvider('getDescribeContainerBuilderTestData')] public function testDescribeContainerBuilder(ContainerBuilder $builder, $expectedDescription, array $options) { $this->assertDescription($expectedDescription, $builder, $options); @@ -101,9 +101,7 @@ public static function getDescribeContainerBuilderTestData(): array return static::getContainerBuilderDescriptionTestData(ObjectsProvider::getContainerBuilders()); } - /** - * @dataProvider getDescribeContainerExistingClassDefinitionTestData - */ + #[DataProvider('getDescribeContainerExistingClassDefinitionTestData')] public function testDescribeContainerExistingClassDefinition(Definition $definition, $expectedDescription) { $this->assertDescription($expectedDescription, $definition); @@ -114,7 +112,7 @@ public static function getDescribeContainerExistingClassDefinitionTestData(): ar return static::getDescriptionTestData(ObjectsProvider::getContainerDefinitionsWithExistingClasses()); } - /** @dataProvider getDescribeContainerDefinitionTestData */ + #[DataProvider('getDescribeContainerDefinitionTestData')] public function testDescribeContainerDefinition(Definition $definition, $expectedDescription) { $this->assertDescription($expectedDescription, $definition); @@ -125,7 +123,7 @@ public static function getDescribeContainerDefinitionTestData(): array return static::getDescriptionTestData(ObjectsProvider::getContainerDefinitions()); } - /** @dataProvider getDescribeContainerDefinitionWithArgumentsShownTestData */ + #[DataProvider('getDescribeContainerDefinitionWithArgumentsShownTestData')] public function testDescribeContainerDefinitionWithArgumentsShown(Definition $definition, $expectedDescription) { $this->assertDescription($expectedDescription, $definition, []); @@ -145,7 +143,7 @@ public static function getDescribeContainerDefinitionWithArgumentsShownTestData( return static::getDescriptionTestData($definitionsWithArgs); } - /** @dataProvider getDescribeContainerAliasTestData */ + #[DataProvider('getDescribeContainerAliasTestData')] public function testDescribeContainerAlias(Alias $alias, $expectedDescription) { $this->assertDescription($expectedDescription, $alias); @@ -156,7 +154,7 @@ public static function getDescribeContainerAliasTestData(): array return static::getDescriptionTestData(ObjectsProvider::getContainerAliases()); } - /** @dataProvider getDescribeContainerDefinitionWhichIsAnAliasTestData */ + #[DataProvider('getDescribeContainerDefinitionWhichIsAnAliasTestData')] public function testDescribeContainerDefinitionWhichIsAnAlias(Alias $alias, $expectedDescription, ContainerBuilder $builder, $options = []) { $this->assertDescription($expectedDescription, $builder, $options); @@ -215,7 +213,7 @@ public static function getDescribeContainerParameterTestData(): array return $data; } - /** @dataProvider getDescribeEventDispatcherTestData */ + #[DataProvider('getDescribeEventDispatcherTestData')] public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options) { $this->assertDescription($expectedDescription, $eventDispatcher, $options); @@ -226,7 +224,7 @@ public static function getDescribeEventDispatcherTestData(): array return static::getEventDispatcherDescriptionTestData(ObjectsProvider::getEventDispatchers()); } - /** @dataProvider getDescribeCallableTestData */ + #[DataProvider('getDescribeCallableTestData')] public function testDescribeCallable($callable, $expectedDescription) { $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,9 +264,7 @@ public static function getClassDescriptionTestData(): array ]; } - /** - * @dataProvider getDeprecationsTestData - */ + #[DataProvider('getDeprecationsTestData')] public function testGetDeprecations(ContainerBuilder $builder, $expectedDescription) { $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/TextDescriptorTest.php b/Tests/Console/Descriptor/TextDescriptorTest.php index 34e16f5e4..0dc4bb18b 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,7 +46,7 @@ public static function getDescribeRouteWithControllerLinkTestData() return $getDescribeData; } - /** @dataProvider getDescribeRouteWithControllerLinkTestData */ + #[DataProvider('getDescribeRouteWithControllerLinkTestData')] public function testDescribeRouteWithControllerLink(Route $route, $expectedDescription) { static::$fileLinkFormatter = new FileLinkFormatter('myeditor://open?file=%f&line=%l'); diff --git a/Tests/Controller/AbstractControllerTest.php b/Tests/Controller/AbstractControllerTest.php index 6ad0113fa..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; @@ -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/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/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/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index c8142e98a..9bf8d5593 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\DBAL\Connection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; use Symfony\Bundle\FullStack; @@ -61,9 +62,7 @@ public function getTestValidSessionName() ]; } - /** - * @dataProvider getTestInvalidSessionName - */ + #[DataProvider('getTestInvalidSessionName')] public function testInvalidSessionName($sessionName) { $processor = new Processor(); @@ -153,9 +152,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 +186,7 @@ public static function provideImportmapPolyfillTests() yield [false, true, false]; } - /** - * @dataProvider provideValidAssetsPackageNameConfigurationTests - */ + #[DataProvider('provideValidAssetsPackageNameConfigurationTests')] public function testValidAssetsPackageNameConfiguration($packageName) { $processor = new Processor(); @@ -221,9 +216,7 @@ public static function provideValidAssetsPackageNameConfigurationTests(): array ]; } - /** - * @dataProvider provideInvalidAssetConfigurationTests - */ + #[DataProvider('provideInvalidAssetConfigurationTests')] public function testInvalidAssetsConfiguration(array $assetConfig, $expectedMessage) { $processor = new Processor(); @@ -275,9 +268,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 +366,7 @@ public function testLockMergeConfigs() ); } - /** - * @dataProvider provideValidSemaphoreConfigurationTests - */ + #[DataProvider('provideValidSemaphoreConfigurationTests')] public function testValidSemaphoreConfiguration($semaphoreConfig, $processedConfig) { $processor = new Processor(); diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index b5f5f1ef5..f7aad3925 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; +use PHPUnit\Framework\Attributes\DataProvider; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LoggerAwareInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; @@ -1934,9 +1935,7 @@ public function testRedisTagAwareAdapter() } } - /** - * @dataProvider appRedisTagAwareConfigProvider - */ + #[DataProvider('appRedisTagAwareConfigProvider')] public function testAppRedisTagAwareAdapter(string $configFile) { $container = $this->createContainerFromFile($configFile); @@ -1980,9 +1979,7 @@ public function testCacheTaggableTagAppliedToPools() } } - /** - * @dataProvider appRedisTagAwareConfigProvider - */ + #[DataProvider('appRedisTagAwareConfigProvider')] public function testCacheTaggableTagAppliedToRedisAwareAppPool(string $configFile) { $container = $this->createContainerFromFile($configFile); @@ -2222,9 +2219,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..d3c1f8ef4 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; @@ -135,9 +136,7 @@ public function testWorkflowValidationStateMachine() }); } - /** - * @dataProvider provideWorkflowValidationCustomTests - */ + #[DataProvider('provideWorkflowValidationCustomTests')] public function testWorkflowValidationCustomBroken(string $class, string $message) { $this->expectException(InvalidConfigurationException::class); @@ -431,9 +430,7 @@ public function testRateLimiterCompoundPolicyInvalidLimiters() }); } - /** - * @dataProvider emailValidationModeProvider - */ + #[DataProvider('emailValidationModeProvider')] public function testValidatorEmailValidationMode(string $mode) { $this->expectNotToPerformAssertions(); diff --git a/Tests/Fixtures/Descriptor/route_1_link.txt b/Tests/Fixtures/Descriptor/route_1_link.txt index ad7a4c8c8..b44fb4dbd 100644 --- a/Tests/Fixtures/Descriptor/route_1_link.txt +++ b/Tests/Fixtures/Descriptor/route_1_link.txt @@ -10,7 +10,7 @@ | 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_link.txt b/Tests/Fixtures/Descriptor/route_2_link.txt index 8e3fe4ca7..f033787a7 100644 --- a/Tests/Fixtures/Descriptor/route_2_link.txt +++ b/Tests/Fixtures/Descriptor/route_2_link.txt @@ -10,7 +10,7 @@ | 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/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..313c6d386 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']); @@ -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, ]; diff --git a/Tests/Functional/CachePoolClearCommandTest.php b/Tests/Functional/CachePoolClearCommandTest.php index a2966b5a2..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 diff --git a/Tests/Functional/CachePoolListCommandTest.php b/Tests/Functional/CachePoolListCommandTest.php index eec484026..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 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 1819e7f4e..6a06e22d7 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,9 +205,7 @@ 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); diff --git a/Tests/Functional/ConfigDumpReferenceCommandTest.php b/Tests/Functional/ConfigDumpReferenceCommandTest.php index a16d8e046..f630173c7 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); @@ -108,10 +99,8 @@ public function testDumpAtPath(bool $debug) , $tester->getDisplay(true)); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testDumpAtPathXml(bool $debug) { $tester = $this->createCommandTester($debug); @@ -125,9 +114,7 @@ public function testDumpAtPathXml(bool $debug) $this->assertStringContainsString('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(bool $debug, array $input, array $expectedSuggestions) { $application = $this->createApplication($debug); diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index 9b70c7bb3..36e730d0a 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']); @@ -282,9 +280,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..4cb989af3 100644 --- a/Tests/Functional/ContainerLintCommandTest.php +++ b/Tests/Functional/ContainerLintCommandTest.php @@ -11,19 +11,17 @@ 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; -/** - * @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([ diff --git a/Tests/Functional/DebugAutowiringCommandTest.php b/Tests/Functional/DebugAutowiringCommandTest.php index b43a12ed6..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,9 +116,7 @@ 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']); diff --git a/Tests/Functional/FragmentTest.php b/Tests/Functional/FragmentTest.php index 48d5c327a..b26601af6 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]); 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/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..88ea3230a 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]); @@ -48,9 +49,8 @@ public function testWelcome($config, $insulate) /** * 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 +72,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 +127,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/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 fd65b18d8..a058d3628 100644 --- a/Tests/Test/WebTestCaseTest.php +++ b/Tests/Test/WebTestCaseTest.php @@ -12,6 +12,7 @@ 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; @@ -192,9 +193,7 @@ public function testAssertBrowserCookieValueSame() $this->getClientTester()->assertBrowserCookieValueSame('foo', 'babar', false, '/path'); } - /** - * @requires function \Symfony\Component\BrowserKit\History::isFirstPage - */ + #[RequiresMethod(History::class, 'isFirstPage')] public function testAssertBrowserHistoryIsOnFirstPage() { $this->createHistoryTester('isFirstPage', true)->assertBrowserHistoryIsOnFirstPage(); @@ -203,9 +202,7 @@ public function testAssertBrowserHistoryIsOnFirstPage() $this->createHistoryTester('isFirstPage', false)->assertBrowserHistoryIsOnFirstPage(); } - /** - * @requires function \Symfony\Component\BrowserKit\History::isFirstPage - */ + #[RequiresMethod(History::class, 'isFirstPage')] public function testAssertBrowserHistoryIsNotOnFirstPage() { $this->createHistoryTester('isFirstPage', false)->assertBrowserHistoryIsNotOnFirstPage(); @@ -214,9 +211,7 @@ public function testAssertBrowserHistoryIsNotOnFirstPage() $this->createHistoryTester('isFirstPage', true)->assertBrowserHistoryIsNotOnFirstPage(); } - /** - * @requires function \Symfony\Component\BrowserKit\History::isLastPage - */ + #[RequiresMethod(History::class, 'isLastPage')] public function testAssertBrowserHistoryIsOnLastPage() { $this->createHistoryTester('isLastPage', true)->assertBrowserHistoryIsOnLastPage(); @@ -225,9 +220,7 @@ public function testAssertBrowserHistoryIsOnLastPage() $this->createHistoryTester('isLastPage', false)->assertBrowserHistoryIsOnLastPage(); } - /** - * @requires function \Symfony\Component\BrowserKit\History::isLastPage - */ + #[RequiresMethod(History::class, 'isLastPage')] public function testAssertBrowserHistoryIsNotOnLastPage() { $this->createHistoryTester('isLastPage', false)->assertBrowserHistoryIsNotOnLastPage(); 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', []); From 6f5f9d2b9c4671b881336afa0a2591ff3d5dc75f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Aug 2025 09:53:42 +0200 Subject: [PATCH 29/85] CS fixes --- Command/ContainerLintCommand.php | 1 - DependencyInjection/FrameworkExtension.php | 16 +++++++----- Resources/config/console.php | 3 ++- .../FrameworkExtensionTestCase.php | 25 +++++++++++-------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Command/ContainerLintCommand.php b/Command/ContainerLintCommand.php index a806c6399..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; diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index e055f5f8b..79bf63d40 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; @@ -133,6 +134,8 @@ use Symfony\Component\Messenger\MessageBusInterface; 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; @@ -176,6 +179,7 @@ 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; @@ -389,7 +393,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".'); } @@ -594,9 +598,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'); @@ -604,9 +608,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'); @@ -1971,7 +1975,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')) { diff --git a/Resources/config/console.php b/Resources/config/console.php index 7ef10bb52..fda2f75d7 100644 --- a/Resources/config/console.php +++ b/Resources/config/console.php @@ -45,6 +45,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; @@ -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/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index f7aad3925..ad1448c71 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -14,6 +14,7 @@ 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; @@ -59,6 +60,10 @@ 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; @@ -599,8 +604,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)); } @@ -611,35 +616,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() From fd6f97536dbc2eaa9871260f0a70be1a9513d994 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Aug 2025 17:29:10 +0200 Subject: [PATCH 30/85] [FrameworkBundle] Decouple ControllerResolverTest from HttpKernel --- Tests/Controller/ControllerResolverTest.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Tests/Controller/ControllerResolverTest.php b/Tests/Controller/ControllerResolverTest.php index 7c7398fd3..ce14ca559 100644 --- a/Tests/Controller/ControllerResolverTest.php +++ b/Tests/Controller/ControllerResolverTest.php @@ -11,15 +11,15 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; +use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface as Psr11ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Tests\Controller\ContainerControllerResolverTest; -class ControllerResolverTest extends ContainerControllerResolverTest +class ControllerResolverTest extends TestCase { public function testAbstractControllerGetsContainerWhenNotSet() { @@ -111,11 +111,6 @@ protected function createControllerResolver(?LoggerInterface $logger = null, ?Ps return new ControllerResolver($container, $logger); } - - protected function createMockParser() - { - return $this->createMock(ControllerNameParser::class); - } } class DummyController extends AbstractController From e1fbaac6099fc213cb82d3fb295e4f51914c3a46 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Aug 2025 17:50:26 +0200 Subject: [PATCH 31/85] Remove some unneeded var annotations --- Command/AssetsInstallCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Command/AssetsInstallCommand.php b/Command/AssetsInstallCommand.php index 5dc8c828e..d8a4f345f 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; /** @@ -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; From 8f2e1890c4921be4b02151e094b13fb8ea3f10d3 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 24 Jun 2025 13:39:09 +0100 Subject: [PATCH 32/85] Remove some implicit bool type juggling --- Command/DebugAutowiringCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/DebugAutowiringCommand.php b/Command/DebugAutowiringCommand.php index 85f546c2f..841c90d5c 100644 --- a/Command/DebugAutowiringCommand.php +++ b/Command/DebugAutowiringCommand.php @@ -184,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 From 6efbca9db75b4e6ed745912df7d3644b47bdf4af Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Thu, 31 Jul 2025 09:35:48 +0200 Subject: [PATCH 33/85] [Mailer] Add MicrosoftGraph API Transport --- DependencyInjection/FrameworkExtension.php | 1 + Resources/config/mailer_transports.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 79bf63d40..521786371 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2887,6 +2887,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', 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, From be639dea579fdcdad8c58af53bebe21e66db5628 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 10 Aug 2025 00:28:14 +0200 Subject: [PATCH 34/85] chore: heredoc indentation as of PHP 7.3 https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc --- Command/AboutCommand.php | 8 +-- Command/AssetsInstallCommand.php | 22 +++---- Command/CacheClearCommand.php | 10 +-- Command/CachePoolClearCommand.php | 6 +- Command/CachePoolDeleteCommand.php | 6 +- Command/CachePoolListCommand.php | 4 +- Command/CachePoolPruneCommand.php | 6 +- Command/CacheWarmupCommand.php | 6 +- Command/ConfigDebugCommand.php | 20 +++--- Command/ConfigDumpReferenceCommand.php | 20 +++--- Command/ContainerDebugCommand.php | 56 ++++++++-------- Command/DebugAutowiringCommand.php | 12 ++-- Command/EventDispatcherDebugCommand.php | 14 ++-- Command/RouterDebugCommand.php | 10 +-- Command/RouterMatchCommand.php | 10 +-- Command/SecretsDecryptToLocalCommand.php | 10 +-- Command/SecretsEncryptFromLocalCommand.php | 6 +- Command/SecretsGenerateKeysCommand.php | 14 ++-- Command/SecretsListCommand.php | 10 +-- Command/SecretsRemoveCommand.php | 6 +- Command/SecretsRevealCommand.php | 6 +- Command/SecretsSetCommand.php | 22 +++---- Command/TranslationDebugCommand.php | 32 ++++----- Command/TranslationExtractCommand.php | 36 +++++----- Command/WorkflowDumpCommand.php | 12 ++-- Command/XliffLintCommand.php | 6 +- Command/YamlLintCommand.php | 6 +- KernelBrowser.php | 24 +++---- Tests/Command/XliffLintCommandTest.php | 6 +- Tests/Command/YamlLintCommandTest.php | 6 +- Tests/Functional/ApiAttributesTest.php | 66 +++++++++---------- .../ConfigDumpReferenceCommandTest.php | 10 +-- .../Functional/ContainerDebugCommandTest.php | 26 ++++---- Tests/Functional/FragmentTest.php | 16 ++--- 34 files changed, 265 insertions(+), 265 deletions(-) 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 d8a4f345f..6fbabc865 100644 --- a/Command/AssetsInstallCommand.php +++ b/Command/AssetsInstallCommand.php @@ -57,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 ) ; } diff --git a/Command/CacheClearCommand.php b/Command/CacheClearCommand.php index 01ddedde3..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 ) ; } 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 b096b0801..8ade22560 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..3a6d1252d 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 ) ; } 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/DebugAutowiringCommand.php b/Command/DebugAutowiringCommand.php index 841c90d5c..5c1869c6a 100644 --- a/Command/DebugAutowiringCommand.php +++ b/Command/DebugAutowiringCommand.php @@ -47,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 ) ; } 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 9740098e5..248f10966 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..0cd780734 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 201fb8be8..06570e9ea 100644 --- a/Command/WorkflowDumpCommand.php +++ b/Command/WorkflowDumpCommand.php @@ -59,13 +59,13 @@ protected function configure(): void 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 + 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 + 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 ) ; } 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/KernelBrowser.php b/KernelBrowser.php index add2508ff..d5b4262a4 100644 --- a/KernelBrowser.php +++ b/KernelBrowser.php @@ -205,25 +205,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/Tests/Command/XliffLintCommandTest.php b/Tests/Command/XliffLintCommandTest.php index ed96fbb00..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()); } diff --git a/Tests/Command/YamlLintCommandTest.php b/Tests/Command/YamlLintCommandTest.php index 30a73015b..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()); } diff --git a/Tests/Functional/ApiAttributesTest.php b/Tests/Functional/ApiAttributesTest.php index 313c6d386..79c8a704b 100644 --- a/Tests/Functional/ApiAttributesTest.php +++ b/Tests/Functional/ApiAttributesTest.php @@ -145,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', @@ -944,11 +944,11 @@ public function __invoke(#[MapRequestPayload] ?RequestBody $body, Request $reque return new Response( << - {$body->comment} - {$body->approved} - - XML + + {$body->comment} + {$body->approved} + + XML ); } } @@ -963,11 +963,11 @@ public function __invoke(Request $request, #[MapRequestPayload] RequestBody $bod return new Response( << - {$body->comment} - {$body->approved} - - XML + + {$body->comment} + {$body->approved} + + XML ); } } @@ -982,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/ConfigDumpReferenceCommandTest.php b/Tests/Functional/ConfigDumpReferenceCommandTest.php index f630173c7..ae5e2d0f7 100644 --- a/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -89,13 +89,13 @@ 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 + EOL , $tester->getDisplay(true)); } diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index 36e730d0a..6e6d053cc 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -166,24 +166,24 @@ 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 + TXT , $tester->getDisplay(true)); putenv('REAL'); diff --git a/Tests/Functional/FragmentTest.php b/Tests/Functional/FragmentTest.php index b26601af6..d35bea23a 100644 --- a/Tests/Functional/FragmentTest.php +++ b/Tests/Functional/FragmentTest.php @@ -26,14 +26,14 @@ public function testFragment($insulate) $client->request('GET', '/fragment_home'); $this->assertEquals(<<getResponse()->getContent()); } From 060b6e6cbe532af6acfa49c45860edff65e3462e Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 1 Aug 2025 14:58:41 +0200 Subject: [PATCH 35/85] run tests with PHPUnit 12.3 --- .../Descriptor/AbstractDescriptorTestCase.php | 30 +++++++++---------- .../Console/Descriptor/TextDescriptorTest.php | 4 +-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index f52a1d8de..a6cbd3085 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -43,7 +43,7 @@ protected function tearDown(): void } #[DataProvider('getDescribeRouteCollectionTestData')] - public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription) + public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $routes); } @@ -54,7 +54,7 @@ public static function getDescribeRouteCollectionTestData(): array } #[DataProvider('getDescribeRouteCollectionWithHttpMethodFilterTestData')] - public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription) + public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $routes, ['method' => $httpMethod]); } @@ -69,7 +69,7 @@ public static function getDescribeRouteCollectionWithHttpMethodFilterTestData(): } #[DataProvider('getDescribeRouteTestData')] - public function testDescribeRoute(Route $route, $expectedDescription) + public function testDescribeRoute(Route $route, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $route); } @@ -80,7 +80,7 @@ public static function getDescribeRouteTestData(): array } #[DataProvider('getDescribeContainerParametersTestData')] - public function testDescribeContainerParameters(ParameterBag $parameters, $expectedDescription) + public function testDescribeContainerParameters(ParameterBag $parameters, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $parameters); } @@ -91,7 +91,7 @@ public static function getDescribeContainerParametersTestData(): array } #[DataProvider('getDescribeContainerBuilderTestData')] - public function testDescribeContainerBuilder(ContainerBuilder $builder, $expectedDescription, array $options) + public function testDescribeContainerBuilder(ContainerBuilder $builder, $expectedDescription, array $options, $file) { $this->assertDescription($expectedDescription, $builder, $options); } @@ -102,7 +102,7 @@ public static function getDescribeContainerBuilderTestData(): array } #[DataProvider('getDescribeContainerExistingClassDefinitionTestData')] - public function testDescribeContainerExistingClassDefinition(Definition $definition, $expectedDescription) + public function testDescribeContainerExistingClassDefinition(Definition $definition, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $definition); } @@ -113,7 +113,7 @@ public static function getDescribeContainerExistingClassDefinitionTestData(): ar } #[DataProvider('getDescribeContainerDefinitionTestData')] - public function testDescribeContainerDefinition(Definition $definition, $expectedDescription) + public function testDescribeContainerDefinition(Definition $definition, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $definition); } @@ -124,7 +124,7 @@ public static function getDescribeContainerDefinitionTestData(): array } #[DataProvider('getDescribeContainerDefinitionWithArgumentsShownTestData')] - public function testDescribeContainerDefinitionWithArgumentsShown(Definition $definition, $expectedDescription) + public function testDescribeContainerDefinitionWithArgumentsShown(Definition $definition, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $definition, []); } @@ -144,7 +144,7 @@ public static function getDescribeContainerDefinitionWithArgumentsShownTestData( } #[DataProvider('getDescribeContainerAliasTestData')] - public function testDescribeContainerAlias(Alias $alias, $expectedDescription) + public function testDescribeContainerAlias(Alias $alias, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $alias); } @@ -155,7 +155,7 @@ public static function getDescribeContainerAliasTestData(): array } #[DataProvider('getDescribeContainerDefinitionWhichIsAnAliasTestData')] - public function testDescribeContainerDefinitionWhichIsAnAlias(Alias $alias, $expectedDescription, ContainerBuilder $builder, $options = []) + public function testDescribeContainerDefinitionWhichIsAnAlias(Alias $alias, $expectedDescription, ContainerBuilder $builder, $options = [], $file = null) { $this->assertDescription($expectedDescription, $builder, $options); } @@ -191,7 +191,7 @@ public static function getDescribeContainerDefinitionWhichIsAnAliasTestData(): a #[IgnoreDeprecations] #[Group('legacy')] #[DataProvider('getDescribeContainerParameterTestData')] - public function testDescribeContainerParameter($parameter, $expectedDescription, array $options) + public function testDescribeContainerParameter($parameter, $expectedDescription, array $options, $file) { $this->assertDescription($expectedDescription, $parameter, $options); } @@ -214,7 +214,7 @@ public static function getDescribeContainerParameterTestData(): array } #[DataProvider('getDescribeEventDispatcherTestData')] - public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options) + public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options, $file) { $this->assertDescription($expectedDescription, $eventDispatcher, $options); } @@ -225,7 +225,7 @@ public static function getDescribeEventDispatcherTestData(): array } #[DataProvider('getDescribeCallableTestData')] - public function testDescribeCallable($callable, $expectedDescription) + public function testDescribeCallable($callable, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $callable); } @@ -238,7 +238,7 @@ public static function getDescribeCallableTestData(): array #[IgnoreDeprecations] #[Group('legacy')] #[DataProvider('getDescribeDeprecatedCallableTestData')] - public function testDescribeDeprecatedCallable($callable, $expectedDescription) + public function testDescribeDeprecatedCallable($callable, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $callable); } @@ -265,7 +265,7 @@ public static function getClassDescriptionTestData(): array } #[DataProvider('getDeprecationsTestData')] - public function testGetDeprecations(ContainerBuilder $builder, $expectedDescription) + public function testGetDeprecations(ContainerBuilder $builder, $expectedDescription, $file) { $this->assertDescription($expectedDescription, $builder, ['deprecations' => true]); } diff --git a/Tests/Console/Descriptor/TextDescriptorTest.php b/Tests/Console/Descriptor/TextDescriptorTest.php index 0dc4bb18b..101ac68a0 100644 --- a/Tests/Console/Descriptor/TextDescriptorTest.php +++ b/Tests/Console/Descriptor/TextDescriptorTest.php @@ -47,10 +47,10 @@ public static function getDescribeRouteWithControllerLinkTestData() } #[DataProvider('getDescribeRouteWithControllerLinkTestData')] - public function testDescribeRouteWithControllerLink(Route $route, $expectedDescription) + 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); } } From 8965ae6d731735ea80ac151d1d127820e8db0c48 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 9 Aug 2025 23:58:04 +0200 Subject: [PATCH 36/85] chore: PHP CS Fixer - restore PHP / PHPUnit rulesets --- Command/AssetsInstallCommand.php | 2 +- .../Compiler/ContainerBuilderDebugDumpPass.php | 2 +- Secrets/SodiumVault.php | 2 +- Tests/Command/AboutCommand/AboutCommandTest.php | 8 ++++---- Tests/Command/TranslationExtractCommandTest.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Command/AssetsInstallCommand.php b/Command/AssetsInstallCommand.php index d8a4f345f..67b5e7653 100644 --- a/Command/AssetsInstallCommand.php +++ b/Command/AssetsInstallCommand.php @@ -237,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/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php index ff9020796..456305bc9 100644 --- a/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php +++ b/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php @@ -59,7 +59,7 @@ public function process(ContainerBuilder $container): void $fs = new Filesystem(); $fs->dumpFile($file, serialize($dump)); - $fs->chmod($file, 0666, umask()); + $fs->chmod($file, 0o666, umask()); } catch (\Throwable $e) { $container->getCompiler()->log($this, $e->getMessage()); // ignore serialization and file-system errors 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/Tests/Command/AboutCommand/AboutCommandTest.php b/Tests/Command/AboutCommand/AboutCommandTest.php index ee3904be3..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()); diff --git a/Tests/Command/TranslationExtractCommandTest.php b/Tests/Command/TranslationExtractCommandTest.php index 89361e825..276af4476 100644 --- a/Tests/Command/TranslationExtractCommandTest.php +++ b/Tests/Command/TranslationExtractCommandTest.php @@ -155,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); } } From ef248dd42df68bd0ae7fe4c045bfa6f3d4fde9cc Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 10 Aug 2025 00:12:49 +0200 Subject: [PATCH 37/85] chore: PHP CS Fixer - update heredoc handling --- Tests/Functional/ConfigDumpReferenceCommandTest.php | 5 +++-- Tests/Functional/ContainerDebugCommandTest.php | 5 +++-- Tests/Functional/FragmentTest.php | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Tests/Functional/ConfigDumpReferenceCommandTest.php b/Tests/Functional/ConfigDumpReferenceCommandTest.php index ae5e2d0f7..377a530e9 100644 --- a/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -95,8 +95,9 @@ public function testDumpAtPath(bool $debug) child2: ~ - EOL - , $tester->getDisplay(true)); + EOL, + $tester->getDisplay(true) + ); } #[TestWith([true])] diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index 6e6d053cc..e1e7ce084 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -183,8 +183,9 @@ public function testDescribeEnvVars() * UNKNOWN - TXT - , $tester->getDisplay(true)); + TXT, + $tester->getDisplay(true) + ); putenv('REAL'); } diff --git a/Tests/Functional/FragmentTest.php b/Tests/Functional/FragmentTest.php index d35bea23a..b8cff1f48 100644 --- a/Tests/Functional/FragmentTest.php +++ b/Tests/Functional/FragmentTest.php @@ -33,8 +33,9 @@ public function testFragment($insulate) es -- fr - TXT - , $client->getResponse()->getContent()); + TXT, + $client->getResponse()->getContent() + ); } public static function getConfigs() From 11a5ef8c4e2c60f1e18d78e1f2b705eb631c252e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 14 Aug 2025 09:36:33 +0200 Subject: [PATCH 38/85] Replace __sleep/wakeup() by __(un)serialize() when BC isn't a concern --- Tests/Functional/app/AppKernel.php | 10 ++++++---- Tests/Kernel/ConcreteMicroKernel.php | 4 ++-- Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index 59c28b2a6..5748b61cd 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -89,14 +89,16 @@ protected function build(ContainerBuilder $container): void $container->registerExtension(new TestDumpExtension()); } - public function __sleep(): array + public function __serialize(): array { - return ['varDir', 'testCase', 'rootConfig', 'environment', 'debug']; + return [$this->varDir, $this->testCase, $this->rootConfig, $this->environment, $this->debug]; } - public function __wakeup(): void + public function __unserialize(array $data): void { - foreach ($this as $k => $v) { + [$this->varDir, $this->testCase, $this->rootConfig, $this->environment, $this->debug] = $data; + + foreach ($this as $v) { if (\is_object($v)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Tests/Kernel/ConcreteMicroKernel.php b/Tests/Kernel/ConcreteMicroKernel.php index a69618099..eac061e3b 100644 --- a/Tests/Kernel/ConcreteMicroKernel.php +++ b/Tests/Kernel/ConcreteMicroKernel.php @@ -55,12 +55,12 @@ public function getLogDir(): string return $this->cacheDir; } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup(): void + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } diff --git a/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php b/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php index 6f7c84d8b..5fa641c7f 100644 --- a/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php +++ b/Tests/Kernel/flex-style/src/FlexStyleMicroKernel.php @@ -64,12 +64,12 @@ public function getProjectDir(): string return \dirname((new \ReflectionObject($this))->getFileName(), 2); } - public function __sleep(): array + public function __serialize(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } - public function __wakeup(): void + public function __unserialize(array $data): void { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } From 1a2c041be5b658b42f17d25f8d73c8006608479e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Aug 2025 18:48:21 +0200 Subject: [PATCH 39/85] Use for options in command description --- Command/TranslationExtractCommand.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Command/TranslationExtractCommand.php b/Command/TranslationExtractCommand.php index 0cd780734..32f19fbe4 100644 --- a/Command/TranslationExtractCommand.php +++ b/Command/TranslationExtractCommand.php @@ -88,7 +88,7 @@ protected function configure(): void 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 + 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) @@ -101,12 +101,12 @@ protected function configure(): void 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 - 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 From beaa29ff13df4fee264283d82fb090746fe2e06b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 20 Aug 2025 12:31:41 +0200 Subject: [PATCH 40/85] Fix low-deps tests --- composer.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3f7a22462..d9d1e34f6 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php": ">=8.2", "composer-runtime-api": ">=2.1", "ext-xml": "*", - "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/cache": "^6.4.12|^7.0|^8.0", "symfony/config": "^7.3|^8.0", "symfony/dependency-injection": "^7.2|^8.0", "symfony/deprecation-contracts": "^2.5|^3", @@ -95,6 +95,7 @@ "symfony/mailer": "<6.4", "symfony/messenger": "<6.4", "symfony/mime": "<6.4", + "symfony/polyfill-php83": "<1.30", "symfony/property-info": "<6.4", "symfony/property-access": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", @@ -109,7 +110,7 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3.0-beta2" + "symfony/workflow": "<7.3" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, From 06a94e25ef07de1dace7072a243c669d6b6428bf Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Thu, 21 Aug 2025 09:25:45 +0200 Subject: [PATCH 41/85] [TypeInfo] Add extra type alias support --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 11 +++++++++++ DependencyInjection/FrameworkExtension.php | 9 ++++++--- Resources/config/schema/symfony-1.0.xsd | 13 ++++++++++++- Resources/config/type_info.php | 5 ++++- Tests/DependencyInjection/ConfigurationTest.php | 1 + .../DependencyInjection/Fixtures/php/type_info.php | 3 +++ .../DependencyInjection/Fixtures/xml/type_info.xml | 4 +++- .../DependencyInjection/Fixtures/yml/type_info.yml | 2 ++ Tests/Functional/TypeInfoTest.php | 6 ++++++ Tests/Functional/app/TypeInfo/Dummy.php | 5 +++++ Tests/Functional/app/TypeInfo/config.yml | 4 +++- 12 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b3cb947..3bd00ccd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * 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 7.3 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 0181b18a1..841e18315 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1304,6 +1304,17 @@ private function addTypeInfoSection(ArrayNodeDefinition $rootNode, callable $ena ->arrayNode('type_info') ->info('Type info configuration') ->{$enableIfStandalone('symfony/type-info', Type::class)}() + ->addDefaultsIfNotSet() + ->fixXmlConfig('alias', 'aliases') + ->children() + ->arrayNode('aliases') + ->info('Additional type aliases to be used during type context creation.') + ->defaultValue([]) + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->scalarPrototype()->end() + ->end() + ->end() ->end() ->end() ; diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index e21b8b838..9db7ecf5f 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -469,7 +469,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) { @@ -2170,7 +2170,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".'); @@ -2179,7 +2179,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')]); @@ -2196,6 +2197,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']); } } diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index a8567aa3e..40399c856 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -382,7 +382,18 @@ - + + + + + + + + + + + + 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/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 9bf8d5593..19d563f52 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -812,6 +812,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/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/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/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/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/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: From 8d17959ce76065f4bbcbaef895ae13b3b6ecab36 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 21 Aug 2025 17:52:30 +0200 Subject: [PATCH 42/85] [Routing][FrameworkBundle] Auto-register routes from attributes found on controller services --- CHANGELOG.md | 1 + DependencyInjection/FrameworkExtension.php | 7 +++- FrameworkBundle.php | 2 + Resources/config/routing.php | 7 ++++ .../AttributeRouteControllerLoaderTest.php | 41 +++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 Tests/Routing/AttributeRouteControllerLoaderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b3cb947..d2dc3b4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.4 --- + * 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()` diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index e21b8b838..7702017e4 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -172,6 +172,7 @@ 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\Scheduler\Attribute\AsCronTask; use Symfony\Component\Scheduler\Attribute\AsPeriodicTask; use Symfony\Component\Scheduler\Attribute\AsSchedule; @@ -768,7 +769,7 @@ public function load(array $configs, ContainerBuilder $container): 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]); @@ -1307,6 +1308,10 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co $loader->load('routing.php'); + if (!class_exists(AttributeServicesLoader::class)) { + $container->removeDefinition('routing.loader.attribute.services'); + } + if ($config['utf8']) { $container->getDefinition('routing.loader')->replaceArgument(1, ['utf8' => true]); } diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 34e8b3ae7..7a262f328 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -61,6 +61,7 @@ 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; @@ -146,6 +147,7 @@ public function build(ContainerBuilder $container): void $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 diff --git a/Resources/config/routing.php b/Resources/config/routing.php index 8cdbbf33a..ad7ace2ca 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'), diff --git a/Tests/Routing/AttributeRouteControllerLoaderTest.php b/Tests/Routing/AttributeRouteControllerLoaderTest.php new file mode 100644 index 000000000..1b24402ab --- /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\Component\Routing\Tests\Fixtures\AttributeFixtures\InvokableController; +use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\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')); + } +} From 3251214bf34f2d1c9dfb6acd23f69d2d992fecd2 Mon Sep 17 00:00:00 2001 From: mamazu <14860264+mamazu@users.noreply.github.com> Date: Fri, 14 Feb 2025 19:20:21 +0100 Subject: [PATCH 43/85] [FrameworkBundle] Only show relevant columns in `debug:router` call and adding colors --- Console/Descriptor/TextDescriptor.php | 87 +++++++++++++++---- Tests/Console/Descriptor/ObjectsProvider.php | 27 +++++- .../Descriptor/empty_route_collection.json | 2 + .../Descriptor/empty_route_collection.md | 0 .../Descriptor/empty_route_collection.txt | 4 + .../Descriptor/empty_route_collection.xml | 3 + Tests/Fixtures/Descriptor/route_1.txt | 2 +- Tests/Fixtures/Descriptor/route_1_link.txt | 2 +- Tests/Fixtures/Descriptor/route_2.txt | 2 +- Tests/Fixtures/Descriptor/route_2_link.txt | 2 +- .../Descriptor/route_collection_1.json | 1 + .../Fixtures/Descriptor/route_collection_1.md | 3 +- .../Descriptor/route_collection_1.txt | 4 +- .../Descriptor/route_collection_1.xml | 1 + .../Descriptor/route_collection_2.txt | 4 +- .../Descriptor/route_collection_3.txt | 2 +- .../Descriptor/route_with_generic_host.json | 18 ++++ .../Descriptor/route_with_generic_host.md | 15 ++++ .../Descriptor/route_with_generic_host.txt | 6 ++ .../Descriptor/route_with_generic_host.xml | 14 +++ .../Descriptor/route_with_generic_scheme.json | 18 ++++ .../Descriptor/route_with_generic_scheme.md | 15 ++++ .../Descriptor/route_with_generic_scheme.txt | 6 ++ .../Descriptor/route_with_generic_scheme.xml | 13 +++ 24 files changed, 221 insertions(+), 30 deletions(-) create mode 100644 Tests/Fixtures/Descriptor/empty_route_collection.json create mode 100644 Tests/Fixtures/Descriptor/empty_route_collection.md create mode 100644 Tests/Fixtures/Descriptor/empty_route_collection.txt create mode 100644 Tests/Fixtures/Descriptor/empty_route_collection.xml create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_host.json create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_host.md create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_host.txt create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_host.xml create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_scheme.json create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_scheme.md create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_scheme.txt create mode 100644 Tests/Fixtures/Descriptor/route_with_generic_scheme.xml diff --git a/Console/Descriptor/TextDescriptor.php b/Console/Descriptor/TextDescriptor.php index a5b31b186..25f727be9 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)], @@ -576,6 +612,21 @@ 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 */ 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/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..f91cf3880 100644 --- a/Tests/Fixtures/Descriptor/route_1_link.txt +++ b/Tests/Fixtures/Descriptor/route_1_link.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 | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | 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..1d9276e34 100644 --- a/Tests/Fixtures/Descriptor/route_2_link.txt +++ b/Tests/Fixtures/Descriptor/route_2_link.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 | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | 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 + + + + + + From 159817a9d5c8ab609e49d3cea3a206a2808d1c6e Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 24 Aug 2025 12:30:22 +0200 Subject: [PATCH 44/85] fix tests --- .../AttributeRouteControllerLoaderTest.php | 4 +-- .../Routing/Fixtures/InvokableController.php | 22 +++++++++++++++ .../Fixtures/MethodActionControllers.php | 28 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 Tests/Routing/Fixtures/InvokableController.php create mode 100644 Tests/Routing/Fixtures/MethodActionControllers.php diff --git a/Tests/Routing/AttributeRouteControllerLoaderTest.php b/Tests/Routing/AttributeRouteControllerLoaderTest.php index 1b24402ab..3f879f78e 100644 --- a/Tests/Routing/AttributeRouteControllerLoaderTest.php +++ b/Tests/Routing/AttributeRouteControllerLoaderTest.php @@ -13,8 +13,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader; -use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\InvokableController; -use Symfony\Component\Routing\Tests\Fixtures\AttributeFixtures\MethodActionControllers; +use Symfony\Bundle\FrameworkBundle\Tests\Routing\Fixtures\InvokableController; +use Symfony\Bundle\FrameworkBundle\Tests\Routing\Fixtures\MethodActionControllers; class AttributeRouteControllerLoaderTest extends TestCase { 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() + { + } +} From 1a7b2cf4040ba4683bcc489949dd480d12676e16 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 26 Aug 2025 13:38:32 +0200 Subject: [PATCH 45/85] add routing.controller to the list of known DI tags --- DependencyInjection/Compiler/UnusedTagsPass.php | 1 + Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php | 1 + 2 files changed, 2 insertions(+) diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 53361e312..8440b3b35 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', 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 From e8540fbb31261eb0540cf1c39ab670035ebfbf2f Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Sat, 23 Aug 2025 00:01:03 +0200 Subject: [PATCH 46/85] [DI]: removed unnecessary checks on `Definition`s and `Alias`es If it is "public", then for sure it is not "private". https://github.com/symfony/symfony/pull/61505#issuecomment-3227850921 --- Console/Descriptor/JsonDescriptor.php | 4 ++-- Console/Descriptor/MarkdownDescriptor.php | 4 ++-- Console/Descriptor/TextDescriptor.php | 4 ++-- Console/Descriptor/XmlDescriptor.php | 4 ++-- .../Compiler/TestServiceContainerWeakRefPass.php | 4 ++-- Tests/DependencyInjection/FrameworkExtensionTestCase.php | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) 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 25f727be9..69e4b395c 100644 --- a/Console/Descriptor/TextDescriptor.php +++ b/Console/Descriptor/TextDescriptor.php @@ -360,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']; @@ -455,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)); diff --git a/Console/Descriptor/XmlDescriptor.php b/Console/Descriptor/XmlDescriptor.php index 6a25ae3a3..08ef443f1 100644 --- a/Console/Descriptor/XmlDescriptor.php +++ b/Console/Descriptor/XmlDescriptor.php @@ -354,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'); @@ -477,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/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/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index ba9732239..ba1a0e4de 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1601,7 +1601,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]); } From a86a38a72f74e43270cc875d2912e710b94b516a Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Fri, 1 Aug 2025 17:52:03 +0200 Subject: [PATCH 47/85] [Form] Add form type guesser for `EnumType` --- DependencyInjection/FrameworkExtension.php | 5 +++++ Resources/config/form.php | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 9db7ecf5f..27c75cc37 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -83,6 +83,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; @@ -880,6 +881,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']); } 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) From 2a2b7cefbea61470fe849479465179229742d9ff Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 25 Aug 2025 18:14:32 +0200 Subject: [PATCH 48/85] [Validator] Allow using attributes to declare compile-time constraint metadata --- CacheWarmer/ValidatorCacheWarmer.php | 7 ++- .../Compiler/UnusedTagsPass.php | 1 + DependencyInjection/FrameworkExtension.php | 31 +++++++--- FrameworkBundle.php | 2 + Resources/config/validator.php | 5 ++ .../FrameworkExtensionTestCase.php | 58 +++++++++++-------- 6 files changed, 67 insertions(+), 37 deletions(-) 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/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 8440b3b35..36e3ee1ae 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -102,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/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index b6395a094..a0a6a86ca 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -216,7 +216,9 @@ use Symfony\Component\Uid\UuidV4; 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; @@ -1801,22 +1803,31 @@ 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'))) { + // 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'); + }); + } + + 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]); } @@ -1855,9 +1866,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) { @@ -2060,7 +2073,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $serializerLoaders = []; - if (isset($config['enable_attributes']) && $config['enable_attributes']) { + if ($config['enable_attributes'] ?? false) { $attributeLoader = new Definition(AttributeLoader::class); $serializerLoaders[] = $attributeLoader; @@ -2100,7 +2113,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'])); } diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 7a262f328..24ca5158a 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -75,6 +75,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; @@ -155,6 +156,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); 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/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index ba1a0e4de..19af06787 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -93,6 +93,7 @@ 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; @@ -1312,16 +1313,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]); @@ -1333,7 +1335,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]); @@ -1408,15 +1410,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); @@ -1434,16 +1440,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); @@ -1490,7 +1499,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]); From a466db3f00024c04b66b75ea477866bdad1fa97d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 28 Aug 2025 10:05:29 +0200 Subject: [PATCH 49/85] [Validator] Add `#[ExtendsValidationFor]` to declare new constraints for a class --- DependencyInjection/FrameworkExtension.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index a0a6a86ca..beba389c8 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -214,6 +214,7 @@ 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; @@ -1819,10 +1820,16 @@ private function registerValidationConfiguration(array $config, ContainerBuilder if (class_exists(ValidatorAttributeMetadataPass::class) && (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug'))) { // 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'); + $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'); } From 0e3a6e31d1cb2f4d71a4d807d0147fbcad68336a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 26 Aug 2025 17:31:17 +0200 Subject: [PATCH 50/85] [Serializer] Allow using attributes to declare compile-time serialization metadata --- CacheWarmer/SerializerCacheWarmer.php | 7 ++-- DependencyInjection/FrameworkExtension.php | 37 +++++++++++++++++-- FrameworkBundle.php | 2 + Resources/config/serializer.php | 4 ++ ...serializer_mapping_without_annotations.php | 2 +- ...serializer_mapping_without_annotations.xml | 2 +- ...serializer_mapping_without_annotations.yml | 2 +- .../FrameworkExtensionTestCase.php | 12 +++--- 8 files changed, 52 insertions(+), 16 deletions(-) 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/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index a0a6a86ca..54a6ae7c2 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -187,9 +187,10 @@ 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\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; @@ -2073,10 +2074,38 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $serializerLoaders = []; - if ($config['enable_attributes'] ?? false) { - $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) { diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 24ca5158a..d8a6d8a4a 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -65,6 +65,7 @@ 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; @@ -170,6 +171,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/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/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/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/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/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 19af06787..5af8f0c8d 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -77,7 +77,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; @@ -1571,7 +1570,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)); } @@ -1761,6 +1760,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']), @@ -1770,15 +1770,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)); } } From 7750b35cd32981bf166c810ae44be65b71251353 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 30 Aug 2025 12:33:44 +0200 Subject: [PATCH 51/85] [Serializer] Add `#[ExtendsSerializationFor]` to declare new serialization attributes for a class --- DependencyInjection/FrameworkExtension.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 54a6ae7c2..aa045b753 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -188,6 +188,7 @@ 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; @@ -2164,6 +2165,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 From 10fc77d110bd1107a4cdea4a58f1ac15780d7125 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 4 Sep 2025 10:58:35 +0200 Subject: [PATCH 52/85] Add global lower bounds to deps on the CI --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index d9d1e34f6..e6af8815c 100644 --- a/composer.json +++ b/composer.json @@ -95,7 +95,6 @@ "symfony/mailer": "<6.4", "symfony/messenger": "<6.4", "symfony/mime": "<6.4", - "symfony/polyfill-php83": "<1.30", "symfony/property-info": "<6.4", "symfony/property-access": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", From a7b9cbf6da809f0cc6ca92f580a139a078829221 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 4 Sep 2025 13:41:00 +0200 Subject: [PATCH 53/85] do not parse attributes on abstract classes with DI < 7.4 --- DependencyInjection/FrameworkExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 54a6ae7c2..66ae9da5b 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -60,6 +60,7 @@ 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\ChildDefinition; @@ -1817,7 +1818,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder // 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'))) { + 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'); From 5b88f3ef30261752412045806367732d40999661 Mon Sep 17 00:00:00 2001 From: valtzu Date: Sun, 23 Mar 2025 14:08:49 +0200 Subject: [PATCH 54/85] [Lock] Add `LockKeyNormalizer` --- DependencyInjection/FrameworkExtension.php | 6 ++++++ Resources/config/lock.php | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 66ae9da5b..daefbf997 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -118,6 +118,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; @@ -2259,6 +2260,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; 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]) ; }; From 42e9ef92de67c6f7ba5c14fdb7824846f7eaed1d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 10 Sep 2025 10:25:20 +0200 Subject: [PATCH 55/85] [Config] Add argument $singular to NodeBuilder::arrayNode() to decouple plurals/singulars from XML --- DependencyInjection/Configuration.php | 264 ++++++------------ .../ConfigBuilderCacheWarmerTest.php | 3 +- composer.json | 2 +- 3 files changed, 88 insertions(+), 181 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index eaed60a9d..e9ebc7695 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -85,8 +85,6 @@ public function getConfigTreeBuilder(): TreeBuilder return $v; }) ->end() - ->fixXmlConfig('enabled_locale') - ->fixXmlConfig('trusted_header') ->children() ->scalarNode('secret')->end() ->booleanNode('http_method_override') @@ -108,7 +106,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() @@ -124,7 +122,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->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() @@ -214,11 +212,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 +272,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 +279,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,9 +360,8 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode): void private function addWorkflowSection(ArrayNodeDefinition $rootNode): void { $rootNode - ->fixXmlConfig('workflow') ->children() - ->arrayNode('workflows') + ->arrayNode('workflows', 'workflow') ->canBeEnabled() ->beforeNormalization() ->always(function ($v) { @@ -401,14 +395,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,7 +419,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() - ->arrayNode('supports') + ->arrayNode('supports', 'support') ->beforeNormalization()->castToArray()->end() ->prototype('scalar') ->cannotBeEmpty() @@ -440,7 +429,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() - ->arrayNode('definition_validators') + ->arrayNode('definition_validators', 'definition_validator') ->prototype('scalar') ->cannotBeEmpty() ->validate() @@ -465,22 +454,17 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->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,7 +476,7 @@ 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() ->then(static function ($places) { @@ -528,7 +512,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->end() - ->arrayNode('transitions') + ->arrayNode('transitions', 'transition') ->beforeNormalization() ->always() ->then(static function ($transitions) { @@ -727,9 +711,8 @@ private function addRequestSection(ArrayNodeDefinition $rootNode): void ->arrayNode('request') ->info('Request configuration') ->canBeEnabled() - ->fixXmlConfig('format') ->children() - ->arrayNode('formats') + ->arrayNode('formats', 'format') ->useAttributeAsKey('name') ->prototype('array') ->beforeNormalization() @@ -753,7 +736,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.') @@ -764,7 +746,7 @@ 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() ->prototype('scalar')->end() @@ -788,13 +770,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.') @@ -810,7 +790,7 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->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() ->prototype('scalar')->end() @@ -849,13 +829,9 @@ 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) @@ -886,7 +862,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']) @@ -909,7 +885,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') @@ -928,7 +904,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') @@ -942,10 +918,8 @@ 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() @@ -954,7 +928,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->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() @@ -975,12 +949,8 @@ 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() ->prototype('scalar')->end() @@ -993,12 +963,11 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->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') @@ -1007,24 +976,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.') @@ -1033,17 +1000,16 @@ 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') ->children() ->variableNode('value')->end() ->stringNode('message')->end() - ->arrayNode('parameters') + ->arrayNode('parameters', 'parameter') ->normalizeKeys(false) ->useAttributeAsKey('name') ->scalarPrototype()->end() @@ -1088,9 +1054,8 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->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() @@ -1143,9 +1108,8 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e }) ->end() ->arrayPrototype() - ->fixXmlConfig('service') ->children() - ->arrayNode('services') + ->arrayNode('services', 'service') ->prototype('scalar')->end() ->end() ->end() @@ -1188,7 +1152,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() @@ -1197,15 +1160,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() @@ -1295,9 +1257,8 @@ private function addTypeInfoSection(ArrayNodeDefinition $rootNode, callable $ena ->info('Type info configuration') ->{$enableIfStandalone('symfony/type-info', Type::class)}() ->addDefaultsIfNotSet() - ->fixXmlConfig('alias', 'aliases') ->children() - ->arrayNode('aliases') + ->arrayNode('aliases', 'alias') ->info('Additional type aliases to be used during type context creation.') ->defaultValue([]) ->normalizeKeys(false) @@ -1317,7 +1278,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.') @@ -1339,16 +1299,15 @@ 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'])) ->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() @@ -1453,9 +1412,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') @@ -1519,9 +1477,8 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->ifTrue(fn ($config) => $config['enabled'] && !$config['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']]) @@ -1578,9 +1535,8 @@ private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $en }) ->end() ->addDefaultsIfNotSet() - ->fixXmlConfig('resource') ->children() - ->arrayNode('resources') + ->arrayNode('resources', 'resource') ->normalizeKeys(false) ->useAttributeAsKey('name') ->requiresAtLeastOneElement() @@ -1628,9 +1584,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.') @@ -1705,7 +1658,7 @@ function ($a) { ->end() ->end() ->end() - ->arrayNode('transports') + ->arrayNode('transports', 'transport') ->normalizeKeys(false) ->useAttributeAsKey('name') ->arrayPrototype() @@ -1715,11 +1668,10 @@ function ($a) { return ['dsn' => $dsn]; }) ->end() - ->fixXmlConfig('option') ->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') @@ -1760,7 +1712,7 @@ 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.') ->beforeNormalization() @@ -1785,7 +1737,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') @@ -1834,10 +1786,9 @@ function ($a) { ]; }) ->end() - ->fixXmlConfig('argument') ->children() ->scalarNode('id')->isRequired()->cannotBeEmpty()->end() - ->arrayNode('arguments') + ->arrayNode('arguments', 'argument') ->normalizeKeys(false) ->defaultValue([]) ->prototype('variable') @@ -1886,7 +1837,6 @@ 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'])) { @@ -1926,15 +1876,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() @@ -2029,11 +1978,10 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->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) { @@ -2088,7 +2036,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) @@ -2194,7 +2142,6 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition return $root ->arrayNode('retry_failed') - ->fixXmlConfig('http_code') ->canBeEnabled() ->addDefaultsIfNotSet() ->beforeNormalization() @@ -2208,7 +2155,7 @@ 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() ->beforeNormalization() ->ifArray() @@ -2233,10 +2180,9 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition ->end() ->useAttributeAsKey('code') ->arrayPrototype() - ->fixXmlConfig('method') ->children() ->integerNode('code')->end() - ->arrayNode('methods') + ->arrayNode('methods', 'method') ->beforeNormalization() ->ifArray() ->then(fn ($v) => array_map('strtoupper', $v)) @@ -2268,22 +2214,18 @@ 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() ->beforeNormalization() ->ifArray() @@ -2291,7 +2233,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->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() @@ -2303,7 +2245,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->end() ->end() ->end() - ->arrayNode('headers') + ->arrayNode('headers', 'header') ->normalizeKeys(false) ->useAttributeAsKey('name') ->prototype('array') @@ -2319,7 +2261,6 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->end() ->arrayNode('dkim_signer') ->addDefaultsIfNotSet() - ->fixXmlConfig('option') ->canBeEnabled() ->info('DKIM signer configuration') ->children() @@ -2334,7 +2275,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') @@ -2414,25 +2355,15 @@ 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') @@ -2440,10 +2371,7 @@ private function addNotifierSection(ArrayNodeDefinition $rootNode, callable $ena ->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() @@ -2505,7 +2433,6 @@ 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) { @@ -2520,7 +2447,7 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ }) ->end() ->children() - ->arrayNode('limiters') + ->arrayNode('limiters', 'limiter') ->useAttributeAsKey('name') ->arrayPrototype() ->children() @@ -2541,7 +2468,7 @@ 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() ->scalarPrototype()->end() @@ -2614,23 +2541,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.') @@ -2640,7 +2554,7 @@ 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) @@ -2655,17 +2569,17 @@ 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() ->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() ->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') @@ -2675,7 +2589,7 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->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') @@ -2685,7 +2599,7 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->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') @@ -2699,45 +2613,39 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->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() + ->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() + ->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() + ->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() + ->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() + ->stringPrototype()->end() ->end() - ->arrayNode('without_attribute_sanitizers') + ->arrayNode('without_attribute_sanitizers', 'without_attribute_sanitizer') ->info('Unregisters custom attribute sanitizers.') - ->scalarPrototype()->end() + ->stringPrototype()->end() ->end() ->integerNode('max_input_length') ->info('The maximum length allowed for the sanitized input.') diff --git a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php index 994151807..931c40df2 100644 --- a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php +++ b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php @@ -412,9 +412,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/composer.json b/composer.json index e6af8815c..3da7bce28 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "composer-runtime-api": ">=2.1", "ext-xml": "*", "symfony/cache": "^6.4.12|^7.0|^8.0", - "symfony/config": "^7.3|^8.0", + "symfony/config": "^7.4|^8.0", "symfony/dependency-injection": "^7.2|^8.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^7.3|^8.0", From db267c9900201063f5ae5359568542c32a72a071 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 21 Mar 2024 13:58:47 +0100 Subject: [PATCH 56/85] [Messenger] Introduce `DefaultStampsProviderInterface ` --- DependencyInjection/FrameworkExtension.php | 5 +++++ Resources/config/messenger.php | 3 +++ Tests/DependencyInjection/FrameworkExtensionTestCase.php | 5 +++++ composer.json | 4 ++-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 4a86068b1..5c3a7cd0c 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -134,6 +134,7 @@ 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; @@ -2436,6 +2437,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 { 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/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 8e50c185b..5c732b188 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1090,6 +1090,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'], @@ -1100,6 +1101,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'], @@ -1132,6 +1134,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'], @@ -1152,6 +1155,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'], @@ -1163,6 +1167,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'], diff --git a/composer.json b/composer.json index e6af8815c..430c0dadc 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "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": "^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", @@ -93,7 +93,7 @@ "symfony/form": "<6.4", "symfony/lock": "<6.4", "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", + "symfony/messenger": "<7.4", "symfony/mime": "<6.4", "symfony/property-info": "<6.4", "symfony/property-access": "<6.4", From 9a8e9e4d17621bb6db4a6d0d947b877ec0e9258f Mon Sep 17 00:00:00 2001 From: cyve Date: Sun, 13 Jul 2025 12:17:34 +0200 Subject: [PATCH 57/85] [FrameworkBundle] Add KernelBrowser::getSession() --- CHANGELOG.md | 1 + KernelBrowser.php | 39 +++++++++++++++++++++++++------- Tests/Functional/SessionTest.php | 14 ++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dec24b35d..d14b197c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * 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()` 7.3 --- diff --git a/KernelBrowser.php b/KernelBrowser.php index d5b4262a4..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; } diff --git a/Tests/Functional/SessionTest.php b/Tests/Functional/SessionTest.php index 88ea3230a..00eb952f0 100644 --- a/Tests/Functional/SessionTest.php +++ b/Tests/Functional/SessionTest.php @@ -45,6 +45,20 @@ 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()); } /** From d5cd6982304b96f55a8dee608af8d6fadd0c9e02 Mon Sep 17 00:00:00 2001 From: Antoine Makdessi Date: Wed, 18 Jun 2025 08:48:28 +0200 Subject: [PATCH 58/85] Introduce `twig-cs-fixer` --- Tests/Functional/app/templates/fragment.html.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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')) -}} From 0f46e0d9784ea84169bd219ee6d3d42708f1b1b4 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Fri, 9 May 2025 17:31:16 -0300 Subject: [PATCH 59/85] [HttpKernel] Add `#[IsSignatureValid]` attribute --- CHANGELOG.md | 1 + DependencyInjection/Compiler/UnusedTagsPass.php | 1 + DependencyInjection/FrameworkExtension.php | 7 +++++++ Resources/config/web.php | 8 ++++++++ 4 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d14b197c4..b41bfd546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Add `assertEmailAddressNotContains()` to the `MailerAssertionsTrait` * Add `framework.type_info.aliases` option * Add `KernelBrowser::getSession()` + * Add autoconfiguration tag `kernel.uri_signer` to `Symfony\Component\HttpFoundation\UriSigner` 7.3 --- diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 36e3ee1ae..359481a68 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -61,6 +61,7 @@ class UnusedTagsPass implements CompilerPassInterface 'kernel.fragment_renderer', 'kernel.locale_aware', 'kernel.reset', + 'kernel.uri_signer', 'ldap', 'mailer.transport_factory', 'messenger.bus', diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 5c3a7cd0c..1d1823215 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -101,6 +101,7 @@ use Symfony\Component\HttpClient\ThrottlingHttpClient; use Symfony\Component\HttpClient\UriTemplateHttpClient; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; @@ -108,6 +109,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; @@ -288,6 +290,9 @@ 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 ($this->hasConsole()) { $loader->load('console.php'); @@ -762,6 +767,8 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('mime.mime_type_guesser'); $container->registerForAutoconfiguration(LoggerAwareInterface::class) ->addMethodCall('setLogger', [new Reference('logger')]); + $container->registerForAutoconfiguration(UriSigner::class) + ->addTag('kernel.uri_signer'); $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector) { $tagAttributes = get_object_vars($attribute); diff --git a/Resources/config/web.php b/Resources/config/web.php index 29e128715..1ffd712b1 100644 --- a/Resources/config/web.php +++ b/Resources/config/web.php @@ -32,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; @@ -148,6 +149,13 @@ ->tag('kernel.event_subscriber') ->tag('kernel.reset', ['method' => '?reset']) + ->set('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class) + ->args([ + service('uri_signer'), + tagged_locator('kernel.uri_signer'), + ]) + ->tag('kernel.event_subscriber') + ->set('controller.helper', ControllerHelper::class) ->tag('container.service_subscriber') From 8f83ad78c26c5f875c81688a9afaf8b1a1386a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Sat, 12 Apr 2025 12:44:38 +0200 Subject: [PATCH 60/85] [FrameworkBundle] Add support for configuring workflow places with glob patterns matching consts/backed enums --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 70 ++++++++++++------- Resources/config/schema/symfony-1.0.xsd | 1 + .../Fixtures/Workflow/Places.php | 10 +++ .../Fixtures/php/workflow_enum.php | 25 +++++++ .../Fixtures/php/workflows.php | 1 + .../Fixtures/xml/workflow_enum.xml | 23 ++++++ .../Fixtures/yml/workflow_enum.yml | 13 ++++ .../FrameworkExtensionTestCase.php | 8 +++ .../PhpFrameworkExtensionTest.php | 2 +- 10 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 Tests/DependencyInjection/Fixtures/Workflow/Places.php create mode 100644 Tests/DependencyInjection/Fixtures/php/workflow_enum.php create mode 100644 Tests/DependencyInjection/Fixtures/xml/workflow_enum.xml create mode 100644 Tests/DependencyInjection/Fixtures/yml/workflow_enum.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index b41bfd546..e322e32b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Add `framework.type_info.aliases` option * Add `KernelBrowser::getSession()` * Add autoconfiguration tag `kernel.uri_signer` to `Symfony\Component\HttpFoundation\UriSigner` + * Add support for configuring workflow places with glob patterns matching consts/backed enums 7.3 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index e9ebc7695..4b23203af 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; @@ -364,7 +365,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->arrayNode('workflows', 'workflow') ->canBeEnabled() ->beforeNormalization() - ->always(function ($v) { + ->always(static function ($v) { if (\is_array($v) && true === $v['enabled']) { $workflows = $v; unset($workflows['enabled']); @@ -478,15 +479,36 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->arrayNode('places', 'place') ->beforeNormalization() - ->always() - ->then(static function ($places) { - if (!\is_array($places)) { - throw new InvalidConfigurationException('The "places" option must be an array in workflow configuration.'); + ->always(static function ($places) { + if (\is_string($places)) { + 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; + } + } + if (!$places) { + throw new InvalidConfigurationException(\sprintf('No places found for pattern "%s::%s" in workflow configuration.', $class, $pattern)); + } + } elseif (!\is_array($places)) { + throw new InvalidConfigurationException('The "places" option must be an array or a "FQCN::glob" pattern in workflow configuration.'); } $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; @@ -514,8 +536,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->arrayNode('transitions', 'transition') ->beforeNormalization() - ->always() - ->then(static function ($transitions) { + ->always(static function ($transitions) { if (!\is_array($transitions)) { throw new InvalidConfigurationException('The "transitions" option must be an array in workflow configuration.'); } @@ -550,14 +571,18 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'') ->end() ->arrayNode('from') - ->beforeNormalization()->castToArray()->end() + ->beforeNormalization() + ->always(static fn ($from) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, \is_array($from) ? $from : [$from])) + ->end() ->requiresAtLeastOneElement() ->prototype('scalar') ->cannotBeEmpty() ->end() ->end() ->arrayNode('to') - ->beforeNormalization()->castToArray()->end() + ->beforeNormalization() + ->always(static fn ($to) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, \is_array($to) ? $to : [$to])) + ->end() ->requiresAtLeastOneElement() ->prototype('scalar') ->cannotBeEmpty() @@ -582,28 +607,23 @@ 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']); - } + ->always(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() diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 24953f8ee..a94430b73 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -475,6 +475,7 @@ + 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', [ + 'workflows' => [ + 'enum' => [ + 'supports' => [ + FrameworkExtensionTestCase::class, + ], + 'places' => Places::cases(), + 'transitions' => [ + 'one' => [ + 'from' => Places::A->value, + 'to' => Places::B->value, + ], + 'two' => [ + 'from' => Places::B->value, + 'to' => Places::C->value, + ], + ], + ] + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/workflows.php b/Tests/DependencyInjection/Fixtures/php/workflows.php index 2c29b8489..c8ca9ea80 100644 --- a/Tests/DependencyInjection/Fixtures/php/workflows.php +++ b/Tests/DependencyInjection/Fixtures/php/workflows.php @@ -1,5 +1,6 @@ loadFromExtension('framework', [ diff --git a/Tests/DependencyInjection/Fixtures/xml/workflow_enum.xml b/Tests/DependencyInjection/Fixtures/xml/workflow_enum.xml new file mode 100644 index 000000000..f5d198b5b --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/workflow_enum.xml @@ -0,0 +1,23 @@ + + + + + + + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase + + a + b + + + b + c + + + + diff --git a/Tests/DependencyInjection/Fixtures/yml/workflow_enum.yml b/Tests/DependencyInjection/Fixtures/yml/workflow_enum.yml new file mode 100644 index 000000000..d72eab0e3 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/workflow_enum.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/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 5c732b188..f5f1008ca 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -519,6 +519,14 @@ public function testWorkflowMultipleTransitionsWithSameName() ], $container->getDefinition($transitions[4])->getArguments()); } + public function testWorkflowEnum() + { + $container = $this->createContainerFromFile('workflow_enum'); + + $workflowDefinition = $container->getDefinition('state_machine.enum.definition'); + $this->assertSame(['a', 'b', 'c'], $workflowDefinition->getArgument(0)); + } + public function testWorkflowGuardExpressions() { $container = $this->createContainerFromFile('workflow_with_guard_expression'); diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index d3c1f8ef4..92a5fd74b 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -75,7 +75,7 @@ public function testAssetPackageCannotHavePathAndUrl() public function testWorkflowValidationPlacesIsArray() { $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The "places" option must be an array in workflow configuration.'); + $this->expectExceptionMessage('The "places" option must be an array or a "FQCN::glob" pattern in workflow configuration.'); $this->createContainerFromClosure(function ($container) { $container->loadFromExtension('framework', [ 'workflows' => [ From 724fcdf977f87540232a92493e533e93612bd317 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 15 Sep 2025 14:15:54 +0200 Subject: [PATCH 61/85] drop the ability to configure a signer with the IsSignatureValid attribute --- CHANGELOG.md | 1 - DependencyInjection/Compiler/UnusedTagsPass.php | 1 - DependencyInjection/FrameworkExtension.php | 3 --- Resources/config/web.php | 1 - 4 files changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e322e32b8..fae3870a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ CHANGELOG * Add `assertEmailAddressNotContains()` to the `MailerAssertionsTrait` * Add `framework.type_info.aliases` option * Add `KernelBrowser::getSession()` - * Add autoconfiguration tag `kernel.uri_signer` to `Symfony\Component\HttpFoundation\UriSigner` * Add support for configuring workflow places with glob patterns matching consts/backed enums 7.3 diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 359481a68..36e3ee1ae 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -61,7 +61,6 @@ class UnusedTagsPass implements CompilerPassInterface 'kernel.fragment_renderer', 'kernel.locale_aware', 'kernel.reset', - 'kernel.uri_signer', 'ldap', 'mailer.transport_factory', 'messenger.bus', diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 1d1823215..fbca404c1 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -101,7 +101,6 @@ use Symfony\Component\HttpClient\ThrottlingHttpClient; use Symfony\Component\HttpClient\UriTemplateHttpClient; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; @@ -767,8 +766,6 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('mime.mime_type_guesser'); $container->registerForAutoconfiguration(LoggerAwareInterface::class) ->addMethodCall('setLogger', [new Reference('logger')]); - $container->registerForAutoconfiguration(UriSigner::class) - ->addTag('kernel.uri_signer'); $container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \ReflectionClass|\ReflectionMethod $reflector) { $tagAttributes = get_object_vars($attribute); diff --git a/Resources/config/web.php b/Resources/config/web.php index 1ffd712b1..17a585d58 100644 --- a/Resources/config/web.php +++ b/Resources/config/web.php @@ -152,7 +152,6 @@ ->set('controller.is_signature_valid_attribute_listener', IsSignatureValidAttributeListener::class) ->args([ service('uri_signer'), - tagged_locator('kernel.uri_signer'), ]) ->tag('kernel.event_subscriber') From eade8a28aaa608841311e2060d89bc55db65e836 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 15 Sep 2025 13:51:35 +0200 Subject: [PATCH 62/85] fix tests --- ...flow_enum.php => workflow_enum_places.php} | 8 +++--- .../Fixtures/php/workflow_glob_places.php | 25 +++++++++++++++++++ .../Fixtures/php/workflows.php | 1 - ...flow_enum.xml => workflow_glob_places.xml} | 0 ...flow_enum.yml => workflow_enum_places.yml} | 0 .../Fixtures/yml/workflow_glob_places.yml | 13 ++++++++++ .../FrameworkExtensionTestCase.php | 16 ++++++++++-- .../XmlFrameworkExtensionTest.php | 5 ++++ composer.json | 2 +- 9 files changed, 62 insertions(+), 8 deletions(-) rename Tests/DependencyInjection/Fixtures/php/{workflow_enum.php => workflow_enum_places.php} (75%) create mode 100644 Tests/DependencyInjection/Fixtures/php/workflow_glob_places.php rename Tests/DependencyInjection/Fixtures/xml/{workflow_enum.xml => workflow_glob_places.xml} (100%) rename Tests/DependencyInjection/Fixtures/yml/{workflow_enum.yml => workflow_enum_places.yml} (100%) create mode 100644 Tests/DependencyInjection/Fixtures/yml/workflow_glob_places.yml diff --git a/Tests/DependencyInjection/Fixtures/php/workflow_enum.php b/Tests/DependencyInjection/Fixtures/php/workflow_enum_places.php similarity index 75% rename from Tests/DependencyInjection/Fixtures/php/workflow_enum.php rename to Tests/DependencyInjection/Fixtures/php/workflow_enum_places.php index 248bf1f8a..cc45f350a 100644 --- a/Tests/DependencyInjection/Fixtures/php/workflow_enum.php +++ b/Tests/DependencyInjection/Fixtures/php/workflow_enum_places.php @@ -12,12 +12,12 @@ 'places' => Places::cases(), 'transitions' => [ 'one' => [ - 'from' => Places::A->value, - 'to' => Places::B->value, + 'from' => Places::A, + 'to' => Places::B, ], 'two' => [ - 'from' => Places::B->value, - 'to' => Places::C->value, + '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/workflows.php b/Tests/DependencyInjection/Fixtures/php/workflows.php index c8ca9ea80..2c29b8489 100644 --- a/Tests/DependencyInjection/Fixtures/php/workflows.php +++ b/Tests/DependencyInjection/Fixtures/php/workflows.php @@ -1,6 +1,5 @@ loadFromExtension('framework', [ diff --git a/Tests/DependencyInjection/Fixtures/xml/workflow_enum.xml b/Tests/DependencyInjection/Fixtures/xml/workflow_glob_places.xml similarity index 100% rename from Tests/DependencyInjection/Fixtures/xml/workflow_enum.xml rename to Tests/DependencyInjection/Fixtures/xml/workflow_glob_places.xml diff --git a/Tests/DependencyInjection/Fixtures/yml/workflow_enum.yml b/Tests/DependencyInjection/Fixtures/yml/workflow_enum_places.yml similarity index 100% rename from Tests/DependencyInjection/Fixtures/yml/workflow_enum.yml rename to Tests/DependencyInjection/Fixtures/yml/workflow_enum_places.yml 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/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index f5f1008ca..88850f15e 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -519,9 +519,21 @@ public function testWorkflowMultipleTransitionsWithSameName() ], $container->getDefinition($transitions[4])->getArguments()); } - public function testWorkflowEnum() + public function testWorkflowEnumPlaces() { - $container = $this->createContainerFromFile('workflow_enum'); + $container = $this->createContainerFromFile('workflow_enum_places'); + + $workflowDefinition = $container->getDefinition('state_machine.enum.definition'); + $this->assertSame(['a', 'b', 'c'], $workflowDefinition->getArgument(0)); + $transitionOne = $container->getDefinition('.state_machine.enum.transition.0'); + $this->assertSame(['one', 'a', 'b'], $transitionOne->getArguments()); + $transitionTwo = $container->getDefinition('.state_machine.enum.transition.1'); + $this->assertSame(['two', 'b', 'c'], $transitionTwo->getArguments()); + } + + public function testWorkflowGlobPlaces() + { + $container = $this->createContainerFromFile('workflow_glob_places'); $workflowDefinition = $container->getDefinition('state_machine.enum.definition'); $this->assertSame(['a', 'b', 'c'], $workflowDefinition->getArgument(0)); diff --git a/Tests/DependencyInjection/XmlFrameworkExtensionTest.php b/Tests/DependencyInjection/XmlFrameworkExtensionTest.php index 1b2eb668a..ff900d617 100644 --- a/Tests/DependencyInjection/XmlFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/XmlFrameworkExtensionTest.php @@ -59,4 +59,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/composer.json b/composer.json index ee8098d78..1fab93dd0 100644 --- a/composer.json +++ b/composer.json @@ -70,7 +70,7 @@ "symfony/type-info": "^7.1.8|^8.0", "symfony/validator": "^7.4|^8.0", "symfony/workflow": "^7.3|^8.0", - "symfony/yaml": "^6.4|^7.0|^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", From b80465c8c1cf8e058d2f5441e49f8e1615334f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sun, 21 Sep 2025 00:36:43 +0200 Subject: [PATCH 63/85] [Config] Fix Yaml dumper for prototyped array with default null --- Tests/Functional/ConfigDumpReferenceCommandTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/Functional/ConfigDumpReferenceCommandTest.php b/Tests/Functional/ConfigDumpReferenceCommandTest.php index 377a530e9..eedcefc5b 100644 --- a/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -115,6 +115,17 @@ public function testDumpAtPathXml(bool $debug) $this->assertStringContainsString('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); } + #[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) { From f144c59016142bdd9aa81a07820e6ef56803b1a3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 4 Aug 2023 11:55:43 +0200 Subject: [PATCH 64/85] [Config] Add `ArrayNodeDefinition::acceptAndWrap()` to list alternative types that should be accepted and wrapped in an array --- DependencyInjection/Configuration.php | 349 +++++++++--------- DependencyInjection/FrameworkExtension.php | 4 - .../PhpFrameworkExtensionTest.php | 30 -- 3 files changed, 173 insertions(+), 210 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b12a0cf82..b3abe323a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -79,9 +79,11 @@ 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; }) @@ -118,8 +120,8 @@ 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() @@ -365,8 +367,9 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->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']); @@ -421,7 +424,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->arrayNode('supports', 'support') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar') ->cannotBeEmpty() ->validate() @@ -451,7 +454,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->cannotBeEmpty() ->end() ->arrayNode('initial_marking') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->defaultValue([]) ->prototype('scalar')->end() ->end() @@ -479,31 +482,31 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->arrayNode('places', 'place') ->beforeNormalization() - ->always(static function ($places) { - if (\is_string($places)) { - 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)); - } + ->ifString() + ->then(static function ($places) { + 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); + $places = []; + $regex = Glob::toRegex($pattern, false); - foreach ((new \ReflectionClass($class))->getConstants() as $name => $value) { - if (preg_match($regex, $name)) { - $places[] = $value; - } - } - if (!$places) { - throw new InvalidConfigurationException(\sprintf('No places found for pattern "%s::%s" in workflow configuration.', $class, $pattern)); + foreach ((new \ReflectionClass($class))->getConstants() as $name => $value) { + if (preg_match($regex, $name)) { + $places[] = $value; } - } elseif (!\is_array($places)) { - throw new InvalidConfigurationException('The "places" option must be an array or a "FQCN::glob" pattern in workflow configuration.'); } + 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 ($value instanceof \BackedEnum) { @@ -536,11 +539,8 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->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)) { @@ -573,8 +573,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->arrayNode('from') ->performNoDeepMerging() + ->acceptAndWrap(['backed-enum', 'string']) ->beforeNormalization() - ->always(static fn ($from) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, \is_array($from) ? $from : [$from])) + ->ifArray() + ->then(static fn ($from) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, $from)) ->end() ->requiresAtLeastOneElement() ->prototype('scalar') @@ -583,8 +585,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->arrayNode('to') ->performNoDeepMerging() + ->acceptAndWrap(['backed-enum', 'string']) ->beforeNormalization() - ->always(static fn ($to) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, \is_array($to) ? $to : [$to])) + ->ifArray() + ->then(static fn ($to) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, $to)) ->end() ->requiresAtLeastOneElement() ->prototype('scalar') @@ -618,7 +622,8 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->thenInvalid('"supports" or "support_strategy" should be configured.') ->end() ->beforeNormalization() - ->always(static function ($values) { + ->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'] = []; @@ -738,11 +743,11 @@ private function addRequestSection(ArrayNodeDefinition $rootNode): void ->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() @@ -771,7 +776,7 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->scalarNode('base_path')->defaultValue('')->end() ->arrayNode('base_urls', 'base_url') ->requiresAtLeastOneElement() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->end() ->end() @@ -806,8 +811,8 @@ 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() @@ -815,7 +820,7 @@ private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enabl ->scalarNode('base_path')->defaultValue('')->end() ->arrayNode('base_urls', 'base_url') ->requiresAtLeastOneElement() - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->end() ->end() @@ -859,9 +864,10 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->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" @@ -947,7 +953,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->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() @@ -975,11 +981,11 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->children() ->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') @@ -1039,10 +1045,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->stringNode('domain')->end() ->end() - ->beforeNormalization() - ->ifTrue(static fn ($v) => !\is_array($v)) - ->then(static fn ($v) => ['value' => $v]) - ->end() + ->acceptAndWrap(['string'], 'value') ->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') @@ -1068,10 +1071,10 @@ 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() @@ -1105,7 +1108,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; @@ -1325,17 +1328,18 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe ->arrayNode('pools', 'pool') ->useAttributeAsKey('name') ->prototype('array') - ->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', '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]; } @@ -1479,20 +1483,18 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->arrayNode('lock') ->info('Lock configuration') ->{$enableIfStandalone('symfony/lock', Lock::class)}() + ->acceptAndWrap(['string'], 'resources') ->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) { + $v += ['enabled' => true]; - return ['enabled' => $e, 'resources' => $v]; + if (!isset($v['resources']) && !isset($v['resource'])) { + $v = ['enabled' => $v['enabled'], 'resources' => $v]; + unset($v['resources']['enabled']); + } + + return $v; }) ->end() ->addDefaultsIfNotSet() @@ -1505,12 +1507,14 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->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']) @@ -1524,7 +1528,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() @@ -1541,20 +1548,18 @@ private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $en ->arrayNode('semaphore') ->info('Semaphore configuration') ->{$enableIfStandalone('symfony/semaphore', Semaphore::class)}() + ->acceptAndWrap(['string'], 'resources') ->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) { + $v += ['enabled' => true]; - return ['enabled' => $e, 'resources' => $v]; + if (!isset($v['resources']) && !isset($v['resource'])) { + $v = ['enabled' => $v['enabled'], 'resources' => $v]; + unset($v['resources']['enabled']); + } + + return $v; }) ->end() ->addDefaultsIfNotSet() @@ -1563,12 +1568,14 @@ private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $en ->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']) @@ -1620,11 +1627,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]; @@ -1685,12 +1689,7 @@ function ($a) { ->normalizeKeys(false) ->useAttributeAsKey('name') ->arrayPrototype() - ->beforeNormalization() - ->ifString() - ->then(function (string $dsn) { - return ['dsn' => $dsn]; - }) - ->end() + ->acceptAndWrap(['string'], 'dsn') ->children() ->scalarNode('dsn')->end() ->scalarNode('serializer')->defaultNull()->info('Service id of a custom serializer to use.')->end() @@ -1706,8 +1705,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.'); } @@ -1738,12 +1739,10 @@ function ($a) { ->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); @@ -1769,13 +1768,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() @@ -1784,18 +1784,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; } @@ -1861,8 +1860,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('HTTP Client configuration') ->{$enableIfStandalone('symfony/http-client', HttpClient::class)}() ->beforeNormalization() - ->always(function ($config) { - if (empty($config['scoped_clients'])) { + ->ifArray() + ->then(static function ($config) { + if (!($config['scoped_clients'] ?? false)) { return $config; } @@ -1921,10 +1921,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; } @@ -2005,22 +2003,17 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->useAttributeAsKey('name') ->normalizeKeys(false) ->arrayPrototype() - ->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() @@ -2045,10 +2038,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; } @@ -2075,10 +2066,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; } @@ -2168,7 +2157,8 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition ->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.'); } @@ -2180,6 +2170,7 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition ->scalarNode('retry_strategy')->defaultNull()->info('service id to override the retry strategy.')->end() ->arrayNode('http_codes', 'http_code') ->performNoDeepMerging() + ->acceptAndWrap(['int', 'string']) ->beforeNormalization() ->ifArray() ->then(static function ($v) { @@ -2206,11 +2197,12 @@ private function createHttpClientRetrySection(): ArrayNodeDefinition ->children() ->integerNode('code')->end() ->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() @@ -2250,9 +2242,10 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->scalarNode('sender')->end() ->arrayNode('recipients', 'recipient') ->performNoDeepMerging() + ->acceptAndWrap(['string']) ->beforeNormalization() ->ifArray() - ->then(fn ($v) => array_filter(array_values($v))) + ->then(static fn ($v) => array_filter(array_values($v))) ->end() ->prototype('scalar')->end() ->end() @@ -2260,9 +2253,10 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->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_filter(array_values($v))) ->end() ->prototype('scalar')->end() ->end() @@ -2273,9 +2267,10 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->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() @@ -2343,10 +2338,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); } @@ -2390,7 +2383,7 @@ private function addNotifierSection(ArrayNodeDefinition $rootNode, callable $ena ->arrayNode('channel_policy') ->useAttributeAsKey('name') ->prototype('array') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->prototype('scalar')->end() ->end() ->end() @@ -2457,16 +2450,14 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->info('Rate limiter configuration') ->{$enableIfStandalone('symfony/rate-limiter', TokenBucketLimiter::class)}() ->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 = ['enabled' => $v['enabled'] ?? true, 'limiters' => $v]; + unset($v['limiters']['enabled']); + } - return $newV; + return $v; }) ->end() ->children() @@ -2493,7 +2484,7 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->end() ->arrayNode('limiters', 'limiter') ->info('The limiter names to use when using the "compound" policy.') - ->beforeNormalization()->castToArray()->end() + ->acceptAndWrap(['string']) ->scalarPrototype()->end() ->end() ->integerNode('limit') @@ -2584,7 +2575,7 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->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)) @@ -2594,13 +2585,13 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->end() ->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', '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', 'allow_attribute') ->info('Configures attributes as allowed. Allowed attributes are attributes the sanitizer should retain from the input.') @@ -2608,7 +2599,7 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->useAttributeAsKey('name') ->variablePrototype() ->beforeNormalization() - ->ifArray()->then(fn ($n) => $n['element'] ?? $n) + ->ifArray()->then(static fn ($n) => $n['element'] ?? $n) ->end() ->end() ->end() @@ -2618,7 +2609,7 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->useAttributeAsKey('name') ->variablePrototype() ->beforeNormalization() - ->ifArray()->then(fn ($n) => $n['element'] ?? $n) + ->ifArray()->then(static fn ($n) => $n['element'] ?? $n) ->end() ->end() ->end() @@ -2629,7 +2620,7 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->arrayPrototype() ->normalizeKeys(false) ->useAttributeAsKey('name') - ->scalarPrototype()->end() + ->stringPrototype()->end() ->end() ->end() ->booleanNode('force_https_urls') @@ -2638,11 +2629,13 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->end() ->arrayNode('allowed_link_schemes', 'allowed_link_scheme') ->info('Allows only a given list of schemes to be used in links href attributes.') + ->acceptAndWrap(['string']) ->stringPrototype()->end() ->end() ->arrayNode('allowed_link_hosts', 'allowed_link_host') ->info('Allows only a given list of hosts to be used in links href attributes.') ->defaultNull() + ->acceptAndWrap(['string']) ->stringPrototype()->end() ->end() ->booleanNode('allow_relative_links') @@ -2651,11 +2644,13 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->end() ->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, ...).') + ->acceptAndWrap(['string']) ->stringPrototype()->end() ->end() ->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, ...).') ->defaultNull() + ->acceptAndWrap(['string']) ->stringPrototype()->end() ->end() ->booleanNode('allow_relative_medias') @@ -2664,10 +2659,12 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->end() ->arrayNode('with_attribute_sanitizers', 'with_attribute_sanitizer') ->info('Registers custom attribute sanitizers.') + ->acceptAndWrap(['string']) ->stringPrototype()->end() ->end() ->arrayNode('without_attribute_sanitizers', 'without_attribute_sanitizer') ->info('Unregisters custom attribute sanitizers.') + ->acceptAndWrap(['string']) ->stringPrototype()->end() ->end() ->integerNode('max_input_length') diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 148475867..f25c41fb3 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2291,10 +2291,6 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont // Generate stores $storeDefinitions = []; foreach ($resourceStores as $resourceStore) { - if (null === $resourceStore) { - $resourceStore = 'null'; - } - $storeDsn = $container->resolveEnvPlaceholders($resourceStore, null, $usedEnvs); if (!$usedEnvs && !str_contains($resourceStore, ':') && !\in_array($resourceStore, ['flock', 'semaphore', 'in-memory', 'null'], true)) { $resourceStore = new Reference($resourceStore); diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 92a5fd74b..8e55f9a8a 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -72,36 +72,6 @@ public function testAssetPackageCannotHavePathAndUrl() }); } - public function testWorkflowValidationPlacesIsArray() - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The "places" option must be an array or a "FQCN::glob" pattern 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); From f981bd2fa540fe68dc9232bdd4849190440b97b8 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 13 Sep 2025 13:18:25 +0200 Subject: [PATCH 65/85] deprecate implicit constraint option names in YAML/XML mapping files --- Tests/Fixtures/Validation/Resources/categories.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 From e26a1c6f7de9198588ae790905a3f861b856e061 Mon Sep 17 00:00:00 2001 From: Andrey Lebedev Date: Thu, 25 Sep 2025 09:06:17 +0400 Subject: [PATCH 66/85] [Notifier][LOX24] Add Lox24 webhook request parser support --- DependencyInjection/FrameworkExtension.php | 1 + Resources/config/notifier_webhook.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f25c41fb3..80141fe9e 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -3287,6 +3287,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', 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') From f086cef11b5ec9d89e13ee19426764985f2a84cb Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 26 Sep 2025 15:50:46 +0200 Subject: [PATCH 67/85] [Config] Config builders should accept booleans for array nodes that can be enabled or disabled --- DependencyInjection/Configuration.php | 41 +++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b3abe323a..cf1783309 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -145,7 +145,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); @@ -1035,6 +1035,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->normalizeKeys(false) ->useAttributeAsKey('name') ->arrayPrototype() + ->acceptAndWrap(['string'], 'value') ->children() ->variableNode('value')->end() ->stringNode('message')->end() @@ -1045,7 +1046,6 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->stringNode('domain')->end() ->end() - ->acceptAndWrap(['string'], 'value') ->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') @@ -1154,7 +1154,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() @@ -1482,16 +1482,17 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->children() ->arrayNode('lock') ->info('Lock configuration') - ->{$enableIfStandalone('symfony/lock', Lock::class)}() ->acceptAndWrap(['string'], 'resources') + ->{$enableIfStandalone('symfony/lock', Lock::class)}() ->beforeNormalization() ->ifArray() ->then(static function ($v) { - $v += ['enabled' => true]; - if (!isset($v['resources']) && !isset($v['resource'])) { - $v = ['enabled' => $v['enabled'], 'resources' => $v]; - unset($v['resources']['enabled']); + $v = ['resources' => $v]; + if (\array_key_exists('enabled', $v['resources'])) { + $v['enabled'] = $v['resources']['enabled']; + unset($v['resources']['enabled']); + } } return $v; @@ -1499,7 +1500,7 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI ->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() ->children() @@ -1547,16 +1548,17 @@ private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $en ->children() ->arrayNode('semaphore') ->info('Semaphore configuration') - ->{$enableIfStandalone('symfony/semaphore', Semaphore::class)}() ->acceptAndWrap(['string'], 'resources') + ->{$enableIfStandalone('symfony/semaphore', Semaphore::class)}() ->beforeNormalization() ->ifArray() ->then(static function ($v) { - $v += ['enabled' => true]; - if (!isset($v['resources']) && !isset($v['resource'])) { - $v = ['enabled' => $v['enabled'], 'resources' => $v]; - unset($v['resources']['enabled']); + $v = ['resources' => $v]; + if (\array_key_exists('enabled', $v['resources'])) { + $v['enabled'] = $v['resources']['enabled']; + unset($v['resources']['enabled']); + } } return $v; @@ -2245,7 +2247,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->acceptAndWrap(['string']) ->beforeNormalization() ->ifArray() - ->then(static fn ($v) => array_filter(array_values($v))) + ->then(static fn ($v) => array_values(array_filter($v))) ->end() ->prototype('scalar')->end() ->end() @@ -2256,7 +2258,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->acceptAndWrap(['string']) ->beforeNormalization() ->ifArray() - ->then(static fn ($v) => array_filter(array_values($v))) + ->then(static fn ($v) => array_values(array_filter($v))) ->end() ->prototype('scalar')->end() ->end() @@ -2453,8 +2455,11 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->ifArray() ->then(static function ($v) { if (!isset($v['limiters']) && !isset($v['limiter'])) { - $v = ['enabled' => $v['enabled'] ?? true, 'limiters' => $v]; - unset($v['limiters']['enabled']); + $v = ['limiters' => $v]; + if (\array_key_exists('enabled', $v['limiters'])) { + $v['enabled'] = $v['limiters']['enabled']; + unset($v['limiters']['enabled']); + } } return $v; From c91c2806e204e850fadfbc60f8721036f0c75ef4 Mon Sep 17 00:00:00 2001 From: Lctrs Date: Wed, 22 Jan 2025 00:49:36 +0100 Subject: [PATCH 68/85] [HttpClient] Make `CachingHttpClient` compatible with RFC 9111 --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 29 +++++++++++++++ DependencyInjection/FrameworkExtension.php | 32 +++++++++++++++++ Resources/config/http_client.php | 9 +++++ Resources/config/schema/symfony-1.0.xsd | 9 +++++ .../Fixtures/php/http_client_caching.php | 24 +++++++++++++ .../Fixtures/xml/http_client_caching.xml | 21 +++++++++++ .../Fixtures/yml/http_client_caching.yml | 19 ++++++++++ .../FrameworkExtensionTestCase.php | 35 +++++++++++++++++++ 9 files changed, 179 insertions(+) create mode 100644 Tests/DependencyInjection/Fixtures/php/http_client_caching.php create mode 100644 Tests/DependencyInjection/Fixtures/xml/http_client_caching.xml create mode 100644 Tests/DependencyInjection/Fixtures/yml/http_client_caching.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index fae3870a4..dbbfe1bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * 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` 7.3 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b3abe323a..215d26793 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1993,6 +1993,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() @@ -2138,6 +2139,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() @@ -2148,6 +2150,33 @@ 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(); diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 80141fe9e..616f6da17 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -93,6 +93,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; @@ -2770,6 +2772,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]; @@ -2793,6 +2797,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); } @@ -2818,6 +2826,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]; @@ -2841,6 +2851,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); } @@ -2882,6 +2896,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)) { 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/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index d65a2f880..49269562a 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -730,6 +730,7 @@ + @@ -757,6 +758,7 @@ + @@ -790,6 +792,13 @@ + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/php/http_client_caching.php b/Tests/DependencyInjection/Fixtures/php/http_client_caching.php new file mode 100644 index 000000000..bcfdbc1da --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/http_client_caching.php @@ -0,0 +1,24 @@ +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/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/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/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 2e3ca854a..4d2f564e9 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -54,6 +54,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\MockHttpClient; use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient; @@ -2185,6 +2187,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'); From e04fc24da4dfc50d8d86f3c536a96ae8ec441d46 Mon Sep 17 00:00:00 2001 From: matlec Date: Sat, 2 Aug 2025 18:45:11 +0200 Subject: [PATCH 69/85] [DependencyInjection][Routing] Deprecate XML configuration format --- Tests/DependencyInjection/XmlFrameworkExtensionTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/DependencyInjection/XmlFrameworkExtensionTest.php b/Tests/DependencyInjection/XmlFrameworkExtensionTest.php index ff900d617..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) From 34d08b7e75ca4edba622dd1656a93268903fd6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 1 Oct 2025 10:07:08 +0200 Subject: [PATCH 70/85] [Workflow] Add support for weighted transitions --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 72 ++++++++-- DependencyInjection/FrameworkExtension.php | 8 +- Resources/config/schema/symfony-1.0.xsd | 12 +- ...th_multiple_transitions_with_same_name.php | 7 +- ...th_multiple_transitions_with_same_name.xml | 7 +- ...th_multiple_transitions_with_same_name.yml | 17 +-- .../FrameworkExtensionTestCase.php | 124 +++++++++++++----- composer.json | 4 +- 9 files changed, 194 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbfe1bcc..fb63d9158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG * 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 7.3 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 4902be08b..9f73bed94 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -562,11 +562,11 @@ 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\'') @@ -576,11 +576,52 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->acceptAndWrap(['backed-enum', 'string']) ->beforeNormalization() ->ifArray() - ->then(static fn ($from) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, $from)) + ->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)) { + return [[ + 'place' => $arcs['value'], + 'weight' => $arcs['weight'], + ]]; + } + + $normalizedArcs = []; + foreach ($arcs as $arc) { + if ($arc instanceof \BackedEnum) { + $arc = $arc->value; + } + if (\is_string($arc)) { + $arc = [ + 'place' => $arc, + 'weight' => 1, + ]; + } 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'], + ]; + } + + $normalizedArcs[] = $arc; + } + + return $normalizedArcs; + }) ->end() ->requiresAtLeastOneElement() - ->prototype('scalar') - ->cannotBeEmpty() + ->prototype('array') + ->children() + ->stringNode('place') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->integerNode('weight') + ->isRequired() + ->end() + ->end() ->end() ->end() ->arrayNode('to') @@ -588,11 +629,26 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->acceptAndWrap(['backed-enum', 'string']) ->beforeNormalization() ->ifArray() - ->then(static fn ($to) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, $to)) + ->then($workflowNormalizeArcs) ->end() ->requiresAtLeastOneElement() - ->prototype('scalar') - ->cannotBeEmpty() + ->prototype('array') + ->children() + ->stringNode('place') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->integerNode('weight') + ->isRequired() + ->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') diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 616f6da17..f2799858b 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -236,6 +236,7 @@ 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; @@ -1114,6 +1115,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) @@ -1137,7 +1143,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']); diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 49269562a..d55d45767 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -518,8 +518,8 @@ - - + + @@ -527,6 +527,14 @@ + + + + + + + + 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/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/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 4d2f564e9..96a750a2b 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -100,6 +100,7 @@ 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; @@ -464,61 +465,104 @@ 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() @@ -527,10 +571,23 @@ public function testWorkflowEnumPlaces() $workflowDefinition = $container->getDefinition('state_machine.enum.definition'); $this->assertSame(['a', 'b', 'c'], $workflowDefinition->getArgument(0)); - $transitionOne = $container->getDefinition('.state_machine.enum.transition.0'); - $this->assertSame(['one', 'a', 'b'], $transitionOne->getArguments()); - $transitionTwo = $container->getDefinition('.state_machine.enum.transition.1'); - $this->assertSame(['two', 'b', 'c'], $transitionTwo->getArguments()); + $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() @@ -622,7 +679,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() diff --git a/composer.json b/composer.json index 1fab93dd0..6ad6e6e59 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "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.3|^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", @@ -109,7 +109,7 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3" + "symfony/workflow": "<7.4" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, From 9c08a647f5fd7c225325b77d10a58cdd2dda26e1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 2 Oct 2025 09:38:40 +0200 Subject: [PATCH 71/85] [DependencyInjection] Deprecate ExtensionInterface::getXsdValidationBasePath() and getNamespace() --- DependencyInjection/FrameworkExtension.php | 6 ++++++ Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php | 6 ++++++ .../ExtensionWithoutConfigTestExtension.php | 6 ++++++ Tests/Functional/app/AppKernel.php | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f2799858b..0a7ca6c56 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -3567,11 +3567,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/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php index 931c40df2..b6b01e6ea 100644 --- a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php +++ b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php @@ -231,11 +231,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'; 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/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; From 5c3d00555264aa22e0ea64c9e8ee3a060dbf652f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 2 Oct 2025 16:59:03 +0200 Subject: [PATCH 72/85] Ensure branch 7.4 will remain compatible with 8.0 once XML loaders are removed --- DependencyInjection/FrameworkExtension.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 0a7ca6c56..fcee372ba 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -180,6 +180,7 @@ 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; @@ -1333,6 +1334,10 @@ 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'); } From 0b7fa5829d6b35464ca720c3a5295ef44c1b48e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 2 Oct 2025 17:43:53 +0200 Subject: [PATCH 73/85] [Workflow] Move the dump command to the component --- Command/WorkflowDumpCommand.php | 116 +--------------------- Resources/config/console.php | 2 +- Tests/Command/WorkflowDumpCommandTest.php | 43 -------- 3 files changed, 6 insertions(+), 155 deletions(-) delete mode 100644 Tests/Command/WorkflowDumpCommandTest.php diff --git a/Command/WorkflowDumpCommand.php b/Command/WorkflowDumpCommand.php index 924f01c18..9d4ca7911 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 \Symfony\Component\Workflow\Command\WorkflowDumpCommand} 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/Resources/config/console.php b/Resources/config/console.php index fda2f75d7..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; @@ -61,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) { diff --git a/Tests/Command/WorkflowDumpCommandTest.php b/Tests/Command/WorkflowDumpCommandTest.php deleted file mode 100644 index d7d17a923..000000000 --- a/Tests/Command/WorkflowDumpCommandTest.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Command; - -use PHPUnit\Framework\Attributes\DataProvider; -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(); - $command = new WorkflowDumpCommand(new ServiceLocator([])); - if (method_exists($application, 'addCommand')) { - $application->addCommand($command); - } else { - $application->add($command); - } - - $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']]; - } -} From d5f5c1a3049d0f38cea141b7ca61910605f834d6 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 3 Oct 2025 08:02:03 +0200 Subject: [PATCH 74/85] Fix CS --- Command/WorkflowDumpCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/WorkflowDumpCommand.php b/Command/WorkflowDumpCommand.php index 9d4ca7911..7cf43b524 100644 --- a/Command/WorkflowDumpCommand.php +++ b/Command/WorkflowDumpCommand.php @@ -17,7 +17,7 @@ trigger_deprecation('symfony/framework-bundle', '7.4', 'The "%s" class is deprecated, use "%s" instead.', WorkflowDumpCommand::class, BaseWorkflowDumpCommand::class); /** - * @deprecated since Symfony 7.4, use {@see \Symfony\Component\Workflow\Command\WorkflowDumpCommand} instead. + * @deprecated since Symfony 7.4, use {@see BaseWorkflowDumpCommand} instead. */ #[AsCommand(name: 'workflow:dump', description: 'Dump a workflow')] class WorkflowDumpCommand extends BaseWorkflowDumpCommand From 0e48857d33438bddca2cd1e81321051621fab82f Mon Sep 17 00:00:00 2001 From: Pierre Ambroise Date: Sun, 27 Jul 2025 22:34:47 +0200 Subject: [PATCH 75/85] Add support for union types on AsEventListener --- CHANGELOG.md | 1 + DependencyInjection/FrameworkExtension.php | 29 ++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb63d9158..8eb71a04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ CHANGELOG * 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` 7.3 --- diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index fcee372ba..37e3459f0 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -774,13 +774,32 @@ 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'); From 063fbe4c282cf1ac838326933871f4d4bdfe542d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 7 Oct 2025 20:19:45 +0200 Subject: [PATCH 76/85] fix compatibility with symfony/dependency-injection 8 --- Command/ConfigDumpReferenceCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/ConfigDumpReferenceCommand.php b/Command/ConfigDumpReferenceCommand.php index 3a6d1252d..934008061 100644 --- a/Command/ConfigDumpReferenceCommand.php +++ b/Command/ConfigDumpReferenceCommand.php @@ -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; } From 52a58837a4b0f93341e83dfb47444cdc0bed0933 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 6 Oct 2025 19:29:01 +0200 Subject: [PATCH 77/85] [HttpFoundation] Add `Request::$allowedHttpMethodOverride` to list which HTTP methods can be overridden --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 7 +++++- DependencyInjection/FrameworkExtension.php | 13 ++++++++-- FrameworkBundle.php | 4 ++++ .../DependencyInjection/ConfigurationTest.php | 1 + .../FrameworkExtensionTestCase.php | 24 +++++++++++++++++++ composer.json | 2 +- 7 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb71a04d..028852416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ CHANGELOG * 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 7.3 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 9f73bed94..bf8a0008b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -91,9 +91,14 @@ public function getConfigTreeBuilder(): TreeBuilder ->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() + ->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)%') diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 37e3459f0..cad1a77e0 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -383,6 +383,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']); @@ -442,7 +443,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); @@ -945,7 +946,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']); @@ -968,6 +969,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 diff --git a/FrameworkBundle.php b/FrameworkBundle.php index d8a6d8a4a..f596a50bd 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -119,6 +119,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(); } diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 19d563f52..1de0fb9d1 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -715,6 +715,7 @@ 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)%', diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 96a750a2b..690333a7d 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -214,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'); diff --git a/composer.json b/composer.json index 6ad6e6e59..9223dae0a 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^7.3|^8.0", "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/http-kernel": "^7.2|^8.0", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php85": "^1.32", From 8da6277ed44addd746ac00ece9491d444990d20a Mon Sep 17 00:00:00 2001 From: matlec Date: Wed, 8 Oct 2025 17:07:28 +0200 Subject: [PATCH 78/85] [Routing] Initialize `router.request_context`'s `_locale` parameter to `%kernel.default_locale%` --- CHANGELOG.md | 1 + Resources/config/routing.php | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 028852416..a93f8fc82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * 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%` 7.3 --- diff --git a/Resources/config/routing.php b/Resources/config/routing.php index ad7ace2ca..7f8d50183 100644 --- a/Resources/config/routing.php +++ b/Resources/config/routing.php @@ -171,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) From 73a0f3fcac65560641828db2bea42fc6267ade39 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 14 Oct 2025 12:43:21 +0200 Subject: [PATCH 79/85] forbid HTTP method override of GET, HEAD, CONNECT and TRACE --- DependencyInjection/Configuration.php | 4 ++++ .../DependencyInjection/ConfigurationTest.php | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index bf8a0008b..465f67e63 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -98,6 +98,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->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.') diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 1de0fb9d1..208ad6a2d 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -13,6 +13,7 @@ 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\FullStack; @@ -711,6 +712,25 @@ 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 [ From 3c519e5130426fe9076e07f7c447f25bce1f6f08 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 16 Oct 2025 13:35:19 +0200 Subject: [PATCH 80/85] [FrameworkBundle] Fix normalization of enums in workflow transitions --- DependencyInjection/Configuration.php | 24 ++++++----- .../DependencyInjection/ConfigurationTest.php | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 465f67e63..e938612bc 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -588,22 +588,18 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->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)) { - return [[ + $arcs = [[ 'place' => $arcs['value'], 'weight' => $arcs['weight'], ]]; + } elseif (\array_key_exists('place', $arcs)) { + $arcs = [$arcs]; } $normalizedArcs = []; foreach ($arcs as $arc) { - if ($arc instanceof \BackedEnum) { - $arc = $arc->value; - } - if (\is_string($arc)) { - $arc = [ - 'place' => $arc, - 'weight' => 1, - ]; + 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)) { @@ -614,6 +610,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ]; } + if (($arc['place'] ?? null) instanceof \BackedEnum) { + $arc['place'] = $arc['place']->value; + } + $normalizedArcs[] = $arc; } @@ -628,7 +628,8 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->cannotBeEmpty() ->end() ->integerNode('weight') - ->isRequired() + ->defaultValue(1) + ->min(1) ->end() ->end() ->end() @@ -648,7 +649,8 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->cannotBeEmpty() ->end() ->integerNode('weight') - ->isRequired() + ->defaultValue(1) + ->min(1) ->end() ->end() ->end() diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 208ad6a2d..47c9aa4c6 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -16,6 +16,7 @@ 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; @@ -696,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(); From 6f9d898cf4af941f9bb0c93f83c0c3ca2490581c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 20 Oct 2025 13:49:43 +0200 Subject: [PATCH 81/85] [FrameworkBundle] Fix testing with Config 8.0 --- Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php index b6b01e6ea..73edadc9a 100644 --- a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php +++ b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php @@ -336,9 +336,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'); } } From 0c00f8728b90efdc9944d2bca6a0f932d561b2ba Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Oct 2025 16:41:08 +0200 Subject: [PATCH 82/85] [FrameworkBundle] Auto-generate `config/reference.php` to assist in writing and discovering app's configuration --- CHANGELOG.md | 1 + .../Compiler/PhpConfigReferenceDumpPass.php | 192 ++++++++++++++ FrameworkBundle.php | 6 +- Kernel/MicroKernelTrait.php | 23 ++ .../PhpConfigReferenceDumpPassTest.php | 158 +++++++++++ Tests/Fixtures/reference.php | 250 ++++++++++++++++++ Tests/Kernel/ConcreteMicroKernel.php | 9 +- Tests/Kernel/MicroKernelTraitTest.php | 33 ++- composer.json | 8 +- 9 files changed, 673 insertions(+), 7 deletions(-) create mode 100644 DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php create mode 100644 Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php create mode 100644 Tests/Fixtures/reference.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3befd90b9..5367397f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 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 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/FrameworkBundle.php b/FrameworkBundle.php index f596a50bd..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; @@ -148,7 +149,10 @@ 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); 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/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php new file mode 100644 index 000000000..c4598c13d --- /dev/null +++ b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php @@ -0,0 +1,158 @@ + + * + * 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() + { + // 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/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/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/composer.json b/composer.json index 9223dae0a..218ab860b 100644 --- a/composer.json +++ b/composer.json @@ -21,17 +21,17 @@ "ext-xml": "*", "symfony/cache": "^6.4.12|^7.0|^8.0", "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.2|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", "symfony/deprecation-contracts": "^2.5|^3", "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/polyfill-php85": "^1.32", - "symfony/filesystem": "^7.1|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", - "symfony/routing": "^6.4|^7.0|^8.0" + "symfony/routing": "^7.4|^8.0" }, "require-dev": { "doctrine/persistence": "^1.3|^2|^3", From ffd4876e2b2930489c0ce483ae63c3b7b53bf6c9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Oct 2025 10:39:47 +0200 Subject: [PATCH 83/85] [Config] Deprecate config builder generators --- CHANGELOG.md | 1 + CacheWarmer/ConfigBuilderCacheWarmer.php | 2 +- DependencyInjection/FrameworkExtension.php | 4 ++++ Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php | 6 +++++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5367397f3..1cd6b1bc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ CHANGELOG * 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 --- 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/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index cd09ca5be..a15495373 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -47,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; @@ -297,6 +298,9 @@ public function load(array $configs, ContainerBuilder $container): void 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'); diff --git a/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php b/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php index 73edadc9a..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; From 5a3428a6741b3374509479d613fbd996f1bda177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 23 Oct 2025 18:26:07 +0200 Subject: [PATCH 84/85] [FrameworkBundle] Fix test on read-only directory on Windows --- .../Compiler/PhpConfigReferenceDumpPassTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php index c4598c13d..7ea127909 100644 --- a/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php +++ b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php @@ -61,6 +61,10 @@ public function testProcessWithConfigDir() 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); From d968f40452bc59ac15a0163294863745c31cc5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 24 Oct 2025 00:40:19 +0200 Subject: [PATCH 85/85] [FrameworkBundle] Skip non-bundle classes in PhpConfigReferenceDumpPass --- .../Compiler/PhpConfigReferenceDumpPass.php | 4 ++++ .../Compiler/PhpConfigReferenceDumpPassTest.php | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php b/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php index cc220eb88..ed1bc3285 100644 --- a/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php +++ b/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\AppReference; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\Routing\Loader\Configurator\RoutesReference; /** @@ -94,6 +95,9 @@ public function process(ContainerBuilder $container): void $anyEnvExtensions = []; foreach ($this->bundlesDefinition as $bundle => $envs) { + if (!is_subclass_of($bundle, BundleInterface::class)) { + continue; + } if (!$extension = (new $bundle())->getContainerExtension()) { continue; } diff --git a/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php index 7ea127909..0f01c5976 100644 --- a/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php +++ b/Tests/DependencyInjection/Compiler/PhpConfigReferenceDumpPassTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PhpConfigReferenceDumpPass; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; @@ -100,6 +101,22 @@ public function testProcessGeneratesExpectedReferenceFile() $this->assertFileEquals(__DIR__.'/../../Fixtures/reference.php', $this->tempDir.'/reference.php'); } + + #[TestWith([self::class])] + #[TestWith(['Symfony\\NotARealClass'])] + public function testProcessWithInvalidBundleClass(string $invalidClass) + { + $container = new ContainerBuilder(); + $container->setParameter('.container.known_envs', ['test', 'dev']); + + $pass = new PhpConfigReferenceDumpPass($this->tempDir.'/reference.php', [ + $invalidClass => ['dev' => true], + ]); + $pass->process($container); + + $referenceFile = $this->tempDir.'/reference.php'; + $this->assertFileExists($referenceFile); + } } class TestBundle extends Bundle