From 85e44d146cfa230353e8638c3fa8e9bfb9c64ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 27 Nov 2024 16:57:40 +0100 Subject: [PATCH 001/111] [AssetMapper] add support for assets pre-compression --- CHANGELOG.md | 5 ++++ DependencyInjection/Configuration.php | 24 +++++++++++++++++++ DependencyInjection/FrameworkExtension.php | 21 ++++++++++++++++ Resources/config/asset_mapper.php | 21 ++++++++++++++++ Resources/config/schema/symfony-1.0.xsd | 11 +++++++++ .../DependencyInjection/ConfigurationTest.php | 11 +++++++++ 6 files changed, 93 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3227eddc2..b0beb9a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.3 +--- + + * Add support for assets pre-compression + 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 678698f4d..372da4a5e 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -17,6 +17,7 @@ use Symfony\Bundle\FullStack; use Symfony\Component\Asset\Package; use Symfony\Component\AssetMapper\AssetMapper; +use Symfony\Component\AssetMapper\Compressor\CompressorInterface; use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeBuilder; @@ -924,6 +925,29 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->info('The directory to store JavaScript vendors.') ->defaultValue('%kernel.project_dir%/assets/vendor') ->end() + ->arrayNode('precompress') + ->info('Precompress assets with Brotli, Zstandard and gzip.') + ->canBeEnabled() + ->fixXmlConfig('format') + ->fixXmlConfig('extension') + ->children() + ->arrayNode('formats') + ->info('Array of formats to enable. "brotli", "zstandard" and "gzip" are supported. Defaults to all formats supported by the system. The entire list must be provided.') + ->prototype('scalar')->end() + ->performNoDeepMerging() + ->validate() + ->ifTrue(static fn (array $v) => array_diff($v, ['brotli', 'zstandard', 'gzip'])) + ->thenInvalid('Unsupported format: "brotli", "zstandard" and "gzip" are supported.') + ->end() + ->end() + ->arrayNode('extensions') + ->info('Array of extensions to compress. The entire list must be provided, no merging occurs.') + ->prototype('scalar')->end() + ->performNoDeepMerging() + ->defaultValue(interface_exists(CompressorInterface::class) ? CompressorInterface::DEFAULT_EXTENSIONS : []) + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 26cae1f30..805d37367 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -32,6 +32,7 @@ use Symfony\Component\Asset\PackageInterface; use Symfony\Component\AssetMapper\AssetMapper; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; +use Symfony\Component\AssetMapper\Compressor\CompressorInterface; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -1372,6 +1373,26 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde ->replaceArgument(3, $config['importmap_polyfill']) ->replaceArgument(4, $config['importmap_script_attributes']) ; + + if (interface_exists(CompressorInterface::class)) { + $compressors = []; + foreach ($config['precompress']['formats'] as $format) { + $compressors[$format] = new Reference("asset_mapper.compressor.$format"); + } + + $container->getDefinition('asset_mapper.compressor')->replaceArgument(0, $compressors ?: null); + + if ($config['precompress']['enabled']) { + $container + ->getDefinition('asset_mapper.local_public_assets_filesystem') + ->addArgument(new Reference('asset_mapper.compressor')) + ->addArgument($config['precompress']['extensions']) + ; + } + } else { + $container->removeDefinition('asset_mapper.compressor'); + $container->removeDefinition('asset_mapper.assets.command.compress'); + } } /** diff --git a/Resources/config/asset_mapper.php b/Resources/config/asset_mapper.php index 404e7af18..c18755864 100644 --- a/Resources/config/asset_mapper.php +++ b/Resources/config/asset_mapper.php @@ -17,6 +17,7 @@ use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\AssetMapperRepository; use Symfony\Component\AssetMapper\Command\AssetMapperCompileCommand; +use Symfony\Component\AssetMapper\Command\CompressAssetsCommand; use Symfony\Component\AssetMapper\Command\DebugAssetMapperCommand; use Symfony\Component\AssetMapper\Command\ImportMapAuditCommand; use Symfony\Component\AssetMapper\Command\ImportMapInstallCommand; @@ -28,6 +29,11 @@ use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler; use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler; use Symfony\Component\AssetMapper\Compiler\SourceMappingUrlsCompiler; +use Symfony\Component\AssetMapper\Compressor\BrotliCompressor; +use Symfony\Component\AssetMapper\Compressor\ChainCompressor; +use Symfony\Component\AssetMapper\Compressor\CompressorInterface; +use Symfony\Component\AssetMapper\Compressor\GzipCompressor; +use Symfony\Component\AssetMapper\Compressor\ZstandardCompressor; use Symfony\Component\AssetMapper\Factory\CachedMappedAssetFactory; use Symfony\Component\AssetMapper\Factory\MappedAssetFactory; use Symfony\Component\AssetMapper\ImportMap\ImportMapAuditor; @@ -254,5 +260,20 @@ ->set('asset_mapper.importmap.command.outdated', ImportMapOutdatedCommand::class) ->args([service('asset_mapper.importmap.update_checker')]) ->tag('console.command') + + ->set('asset_mapper.compressor.brotli', BrotliCompressor::class) + ->set('asset_mapper.compressor.zstandard', ZstandardCompressor::class) + ->set('asset_mapper.compressor.gzip', GzipCompressor::class) + + ->set('asset_mapper.compressor', ChainCompressor::class) + ->args([ + abstract_arg('compressor'), + service('logger'), + ]) + ->alias(CompressorInterface::class, 'asset_mapper.compressor') + + ->set('asset_mapper.assets.command.compress', CompressAssetsCommand::class) + ->args([service('asset_mapper.compressor')]) + ->tag('console.command') ; }; diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index ed7cc744f..91528b60f 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -206,6 +206,7 @@ + @@ -230,6 +231,16 @@ + + + + + + + + + + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 53706d2e0..c7113cfb4 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; use Symfony\Bundle\FullStack; +use Symfony\Component\AssetMapper\Compressor\CompressorInterface; use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; @@ -141,6 +142,11 @@ public function testAssetMapperCanBeEnabled() 'vendor_dir' => '%kernel.project_dir%/assets/vendor', 'importmap_script_attributes' => [], 'exclude_dotfiles' => true, + 'precompress' => [ + 'enabled' => false, + 'formats' => [], + 'extensions' => interface_exists(CompressorInterface::class) ? CompressorInterface::DEFAULT_EXTENSIONS : [], + ], ]; $this->assertEquals($defaultConfig, $config['asset_mapper']); @@ -847,6 +853,11 @@ protected static function getBundleDefaultConfig() 'vendor_dir' => '%kernel.project_dir%/assets/vendor', 'importmap_script_attributes' => [], 'exclude_dotfiles' => true, + 'precompress' => [ + 'enabled' => false, + 'formats' => [], + 'extensions' => interface_exists(CompressorInterface::class) ? CompressorInterface::DEFAULT_EXTENSIONS : [], + ], ], 'cache' => [ 'pools' => [], From e56da792d24e84157cf8311c8594b967b4b46516 Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Fri, 6 Dec 2024 09:30:19 +0100 Subject: [PATCH 002/111] Rename TranslationUpdateCommand to TranslationExtract command to match the command name --- CHANGELOG.md | 1 + Command/TranslationExtractCommand.php | 499 ++++++++++++++++++ Command/TranslationUpdateCommand.php | 473 +---------------- Resources/config/console.php | 4 +- ...anslationExtractCommandCompletionTest.php} | 6 +- ....php => TranslationExtractCommandTest.php} | 12 +- 6 files changed, 514 insertions(+), 481 deletions(-) create mode 100644 Command/TranslationExtractCommand.php rename Tests/Command/{TranslationUpdateCommandCompletionTest.php => TranslationExtractCommandCompletionTest.php} (93%) rename Tests/Command/{TranslationUpdateCommandTest.php => TranslationExtractCommandTest.php} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0beb9a96..3f05ad7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add support for assets pre-compression + * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` 7.2 --- diff --git a/Command/TranslationExtractCommand.php b/Command/TranslationExtractCommand.php new file mode 100644 index 000000000..52f8d0c73 --- /dev/null +++ b/Command/TranslationExtractCommand.php @@ -0,0 +1,499 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Translation\Catalogue\MergeOperation; +use Symfony\Component\Translation\Catalogue\TargetOperation; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +/** + * A command that parses templates to extract translation messages and adds them + * into the translation files. + * + * @author Michel Salib + */ +#[AsCommand(name: 'translation:extract', description: 'Extract missing translations keys from code to translation files')] +class TranslationExtractCommand extends Command +{ + private const ASC = 'asc'; + private const DESC = 'desc'; + private const SORT_ORDERS = [self::ASC, self::DESC]; + private const FORMATS = [ + 'xlf12' => ['xlf', '1.2'], + 'xlf20' => ['xlf', '2.0'], + ]; + private const NO_FILL_PREFIX = "\0NoFill\0"; + + public function __construct( + private TranslationWriterInterface $writer, + private TranslationReaderInterface $reader, + private ExtractorInterface $extractor, + private string $defaultLocale, + private ?string $defaultTransPath = null, + private ?string $defaultViewsPath = null, + private array $transPaths = [], + private array $codePaths = [], + private array $enabledLocales = [], + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), + new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'), + new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'), + new InputOption('no-fill', null, InputOption::VALUE_NONE, 'Extract translation keys without filling in values'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'), + new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), + new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), + new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), + new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically'), + new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), + ]) + ->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. + +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) + + php %command.full_name% --dump-messages en AcmeBundle + php %command.full_name% --force --prefix="new_" fr AcmeBundle + +Example running against default messages directory + + php %command.full_name% --dump-messages en + php %command.full_name% --force --prefix="new_" fr + +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: + + php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle + +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $errorIo = $io->getErrorStyle(); + + // check presence of force or dump-message + if (true !== $input->getOption('force') && true !== $input->getOption('dump-messages')) { + $errorIo->error('You must choose one of --force or --dump-messages'); + + return 1; + } + + $format = $input->getOption('format'); + $xliffVersion = '1.2'; + + if (\array_key_exists($format, self::FORMATS)) { + [$format, $xliffVersion] = self::FORMATS[$format]; + } + + // check format + $supportedFormats = $this->writer->getFormats(); + if (!\in_array($format, $supportedFormats, true)) { + $errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).', xlf12 and xlf20.']); + + return 1; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + + // Define Root Paths + $transPaths = $this->getRootTransPaths(); + $codePaths = $this->getRootCodePaths($kernel); + + $currentName = 'default directory'; + + // Override with provided Bundle info + if (null !== $input->getArgument('bundle')) { + try { + $foundBundle = $kernel->getBundle($input->getArgument('bundle')); + $bundleDir = $foundBundle->getPath(); + $transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations']; + $codePaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + $currentName = $foundBundle->getName(); + } catch (\InvalidArgumentException) { + // such a bundle does not exist, so treat the argument as path + $path = $input->getArgument('bundle'); + + $transPaths = [$path.'/translations']; + $codePaths = [$path.'/templates']; + + if (!is_dir($transPaths[0])) { + throw new InvalidArgumentException(\sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); + } + } + } + + $io->title('Translation Messages Extractor and Dumper'); + $io->comment(\sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); + + $io->comment('Parsing templates...'); + $prefix = $input->getOption('no-fill') ? self::NO_FILL_PREFIX : $input->getOption('prefix'); + $extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $prefix); + + $io->comment('Loading translation files...'); + $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths); + + if (null !== $domain = $input->getOption('domain')) { + $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); + $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain); + } + + // process catalogues + $operation = $input->getOption('clean') + ? new TargetOperation($currentCatalogue, $extractedCatalogue) + : new MergeOperation($currentCatalogue, $extractedCatalogue); + + // Exit if no messages found. + if (!\count($operation->getDomains())) { + $errorIo->warning('No translation messages were found.'); + + return 0; + } + + $resultMessage = 'Translation files were successfully updated'; + + $operation->moveMessagesToIntlDomainsIfPossible('new'); + + if ($sort = $input->getOption('sort')) { + $sort = strtolower($sort); + if (!\in_array($sort, self::SORT_ORDERS, true)) { + $errorIo->error(['Wrong sort order', 'Supported formats are: '.implode(', ', self::SORT_ORDERS).'.']); + + return 1; + } + } + + // show compiled list of messages + if (true === $input->getOption('dump-messages')) { + $extractedMessagesCount = 0; + $io->newLine(); + foreach ($operation->getDomains() as $domain) { + $newKeys = array_keys($operation->getNewMessages($domain)); + $allKeys = array_keys($operation->getMessages($domain)); + + $list = array_merge( + array_diff($allKeys, $newKeys), + array_map(fn ($id) => \sprintf('%s', $id), $newKeys), + array_map(fn ($id) => \sprintf('%s', $id), array_keys($operation->getObsoleteMessages($domain))) + ); + + $domainMessagesCount = \count($list); + + if (self::DESC === $sort) { + rsort($list); + } else { + sort($list); + } + + $io->section(\sprintf('Messages extracted for domain "%s" (%d message%s)', $domain, $domainMessagesCount, $domainMessagesCount > 1 ? 's' : '')); + $io->listing($list); + + $extractedMessagesCount += $domainMessagesCount; + } + + if ('xlf' === $format) { + $io->comment(\sprintf('Xliff output version is %s', $xliffVersion)); + } + + $resultMessage = \sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was'); + } + + // save the files + if (true === $input->getOption('force')) { + $io->comment('Writing files...'); + + $bundleTransPath = false; + foreach ($transPaths as $path) { + if (is_dir($path)) { + $bundleTransPath = $path; + } + } + + if (!$bundleTransPath) { + $bundleTransPath = end($transPaths); + } + + $operationResult = $operation->getResult(); + if ($sort) { + $operationResult = $this->sortCatalogue($operationResult, $sort); + } + + if (true === $input->getOption('no-fill')) { + $this->removeNoFillTranslations($operationResult); + } + + $this->writer->write($operationResult, $format, ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $xliffVersion, 'as_tree' => $input->getOption('as-tree'), 'inline' => $input->getOption('as-tree') ?? 0]); + + if (true === $input->getOption('dump-messages')) { + $resultMessage .= ' and translation files were updated'; + } + } + + $io->success($resultMessage.'.'); + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('locale')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + /** @var KernelInterface $kernel */ + $kernel = $this->getApplication()->getKernel(); + if ($input->mustSuggestArgumentValuesFor('bundle')) { + $bundles = []; + + foreach ($kernel->getBundles() as $bundle) { + $bundles[] = $bundle->getName(); + if ($bundle->getContainerExtension()) { + $bundles[] = $bundle->getContainerExtension()->getAlias(); + } + } + + $suggestions->suggestValues($bundles); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(array_merge( + $this->writer->getFormats(), + array_keys(self::FORMATS) + )); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domain') && $locale = $input->getArgument('locale')) { + $extractedCatalogue = $this->extractMessages($locale, $this->getRootCodePaths($kernel), $input->getOption('prefix')); + + $currentCatalogue = $this->loadCurrentMessages($locale, $this->getRootTransPaths()); + + // process catalogues + $operation = $input->getOption('clean') + ? new TargetOperation($currentCatalogue, $extractedCatalogue) + : new MergeOperation($currentCatalogue, $extractedCatalogue); + + $suggestions->suggestValues($operation->getDomains()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('sort')) { + $suggestions->suggestValues(self::SORT_ORDERS); + } + } + + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue + { + $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); + + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + $filteredCatalogue->add($messages, $domain); + } + foreach ($catalogue->getResources() as $resource) { + $filteredCatalogue->addResource($resource); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $domain); + } + } + + return $filteredCatalogue; + } + + private function sortCatalogue(MessageCatalogue $catalogue, string $sort): MessageCatalogue + { + $sortedCatalogue = new MessageCatalogue($catalogue->getLocale()); + + foreach ($catalogue->getDomains() as $domain) { + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + if (self::DESC === $sort) { + krsort($intlMessages); + } elseif (self::ASC === $sort) { + ksort($intlMessages); + } + + $sortedCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + if (self::DESC === $sort) { + krsort($messages); + } elseif (self::ASC === $sort) { + ksort($messages); + } + + $sortedCatalogue->add($messages, $domain); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $sortedCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $sortedCatalogue->setMetadata($k, $v, $domain); + } + } + } + + foreach ($catalogue->getResources() as $resource) { + $sortedCatalogue->addResource($resource); + } + + return $sortedCatalogue; + } + + private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue + { + $extractedCatalogue = new MessageCatalogue($locale); + $this->extractor->setPrefix($prefix); + $transPaths = $this->filterDuplicateTransPaths($transPaths); + foreach ($transPaths as $path) { + if (is_dir($path) || is_file($path)) { + $this->extractor->extract($path, $extractedCatalogue); + } + } + + return $extractedCatalogue; + } + + private function filterDuplicateTransPaths(array $transPaths): array + { + $transPaths = array_filter(array_map('realpath', $transPaths)); + + sort($transPaths); + + $filteredPaths = []; + + foreach ($transPaths as $path) { + foreach ($filteredPaths as $filteredPath) { + if (str_starts_with($path, $filteredPath.\DIRECTORY_SEPARATOR)) { + continue 2; + } + } + + $filteredPaths[] = $path; + } + + return $filteredPaths; + } + + private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue + { + $currentCatalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + if (is_dir($path)) { + $this->reader->read($path, $currentCatalogue); + } + } + + return $currentCatalogue; + } + + private function getRootTransPaths(): array + { + $transPaths = $this->transPaths; + if ($this->defaultTransPath) { + $transPaths[] = $this->defaultTransPath; + } + + return $transPaths; + } + + private function getRootCodePaths(KernelInterface $kernel): array + { + $codePaths = $this->codePaths; + $codePaths[] = $kernel->getProjectDir().'/src'; + if ($this->defaultViewsPath) { + $codePaths[] = $this->defaultViewsPath; + } + + return $codePaths; + } + + private function removeNoFillTranslations(MessageCatalogueInterface $operation): void + { + foreach ($operation->all('messages') as $key => $message) { + if (str_starts_with($message, self::NO_FILL_PREFIX)) { + $operation->set($key, '', 'messages'); + } + } + } +} diff --git a/Command/TranslationUpdateCommand.php b/Command/TranslationUpdateCommand.php index b26d3f9ad..de5aa9389 100644 --- a/Command/TranslationUpdateCommand.php +++ b/Command/TranslationUpdateCommand.php @@ -11,45 +11,12 @@ 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\Console\Style\SymfonyStyle; -use Symfony\Component\HttpKernel\KernelInterface; -use Symfony\Component\Translation\Catalogue\MergeOperation; -use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\Extractor\ExtractorInterface; -use Symfony\Component\Translation\MessageCatalogue; -use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriterInterface; -/** - * A command that parses templates to extract translation messages and adds them - * into the translation files. - * - * @author Michel Salib - * - * @final - */ -#[AsCommand(name: 'translation:extract', description: 'Extract missing translations keys from code to translation files')] -class TranslationUpdateCommand extends Command +class TranslationUpdateCommand extends TranslationExtractCommand { - private const ASC = 'asc'; - private const DESC = 'desc'; - private const SORT_ORDERS = [self::ASC, self::DESC]; - private const FORMATS = [ - 'xlf12' => ['xlf', '1.2'], - 'xlf20' => ['xlf', '2.0'], - ]; - private const NO_FILL_PREFIX = "\0NoFill\0"; - public function __construct( private TranslationWriterInterface $writer, private TranslationReaderInterface $reader, @@ -61,441 +28,7 @@ public function __construct( private array $codePaths = [], private array $enabledLocales = [], ) { - parent::__construct(); - } - - protected function configure(): void - { - $this - ->setDefinition([ - new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), - new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'), - new InputOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Override the default prefix', '__'), - new InputOption('no-fill', null, InputOption::VALUE_NONE, 'Extract translation keys without filling in values'), - new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format', 'xlf12'), - new InputOption('dump-messages', null, InputOption::VALUE_NONE, 'Should the messages be dumped in the console'), - new InputOption('force', null, InputOption::VALUE_NONE, 'Should the extract be done'), - new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), - new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to extract'), - new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically'), - new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'), - ]) - ->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. - -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) - - php %command.full_name% --dump-messages en AcmeBundle - php %command.full_name% --force --prefix="new_" fr AcmeBundle - -Example running against default messages directory - - php %command.full_name% --dump-messages en - php %command.full_name% --force --prefix="new_" fr - -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: - - php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle - -EOF - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $errorIo = $io->getErrorStyle(); - - // check presence of force or dump-message - if (true !== $input->getOption('force') && true !== $input->getOption('dump-messages')) { - $errorIo->error('You must choose one of --force or --dump-messages'); - - return 1; - } - - $format = $input->getOption('format'); - $xliffVersion = '1.2'; - - if (\array_key_exists($format, self::FORMATS)) { - [$format, $xliffVersion] = self::FORMATS[$format]; - } - - // check format - $supportedFormats = $this->writer->getFormats(); - if (!\in_array($format, $supportedFormats, true)) { - $errorIo->error(['Wrong output format', 'Supported formats are: '.implode(', ', $supportedFormats).', xlf12 and xlf20.']); - - return 1; - } - - /** @var KernelInterface $kernel */ - $kernel = $this->getApplication()->getKernel(); - - // Define Root Paths - $transPaths = $this->getRootTransPaths(); - $codePaths = $this->getRootCodePaths($kernel); - - $currentName = 'default directory'; - - // Override with provided Bundle info - if (null !== $input->getArgument('bundle')) { - try { - $foundBundle = $kernel->getBundle($input->getArgument('bundle')); - $bundleDir = $foundBundle->getPath(); - $transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations']; - $codePaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - if ($this->defaultViewsPath) { - $codePaths[] = $this->defaultViewsPath; - } - $currentName = $foundBundle->getName(); - } catch (\InvalidArgumentException) { - // such a bundle does not exist, so treat the argument as path - $path = $input->getArgument('bundle'); - - $transPaths = [$path.'/translations']; - $codePaths = [$path.'/templates']; - - if (!is_dir($transPaths[0])) { - throw new InvalidArgumentException(\sprintf('"%s" is neither an enabled bundle nor a directory.', $transPaths[0])); - } - } - } - - $io->title('Translation Messages Extractor and Dumper'); - $io->comment(\sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); - - $io->comment('Parsing templates...'); - $prefix = $input->getOption('no-fill') ? self::NO_FILL_PREFIX : $input->getOption('prefix'); - $extractedCatalogue = $this->extractMessages($input->getArgument('locale'), $codePaths, $prefix); - - $io->comment('Loading translation files...'); - $currentCatalogue = $this->loadCurrentMessages($input->getArgument('locale'), $transPaths); - - if (null !== $domain = $input->getOption('domain')) { - $currentCatalogue = $this->filterCatalogue($currentCatalogue, $domain); - $extractedCatalogue = $this->filterCatalogue($extractedCatalogue, $domain); - } - - // process catalogues - $operation = $input->getOption('clean') - ? new TargetOperation($currentCatalogue, $extractedCatalogue) - : new MergeOperation($currentCatalogue, $extractedCatalogue); - - // Exit if no messages found. - if (!\count($operation->getDomains())) { - $errorIo->warning('No translation messages were found.'); - - return 0; - } - - $resultMessage = 'Translation files were successfully updated'; - - $operation->moveMessagesToIntlDomainsIfPossible('new'); - - if ($sort = $input->getOption('sort')) { - $sort = strtolower($sort); - if (!\in_array($sort, self::SORT_ORDERS, true)) { - $errorIo->error(['Wrong sort order', 'Supported formats are: '.implode(', ', self::SORT_ORDERS).'.']); - - return 1; - } - } - - // show compiled list of messages - if (true === $input->getOption('dump-messages')) { - $extractedMessagesCount = 0; - $io->newLine(); - foreach ($operation->getDomains() as $domain) { - $newKeys = array_keys($operation->getNewMessages($domain)); - $allKeys = array_keys($operation->getMessages($domain)); - - $list = array_merge( - array_diff($allKeys, $newKeys), - array_map(fn ($id) => \sprintf('%s', $id), $newKeys), - array_map(fn ($id) => \sprintf('%s', $id), array_keys($operation->getObsoleteMessages($domain))) - ); - - $domainMessagesCount = \count($list); - - if (self::DESC === $sort) { - rsort($list); - } else { - sort($list); - } - - $io->section(\sprintf('Messages extracted for domain "%s" (%d message%s)', $domain, $domainMessagesCount, $domainMessagesCount > 1 ? 's' : '')); - $io->listing($list); - - $extractedMessagesCount += $domainMessagesCount; - } - - if ('xlf' === $format) { - $io->comment(\sprintf('Xliff output version is %s', $xliffVersion)); - } - - $resultMessage = \sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was'); - } - - // save the files - if (true === $input->getOption('force')) { - $io->comment('Writing files...'); - - $bundleTransPath = false; - foreach ($transPaths as $path) { - if (is_dir($path)) { - $bundleTransPath = $path; - } - } - - if (!$bundleTransPath) { - $bundleTransPath = end($transPaths); - } - - $operationResult = $operation->getResult(); - if ($sort) { - $operationResult = $this->sortCatalogue($operationResult, $sort); - } - - if (true === $input->getOption('no-fill')) { - $this->removeNoFillTranslations($operationResult); - } - - $this->writer->write($operationResult, $format, ['path' => $bundleTransPath, 'default_locale' => $this->defaultLocale, 'xliff_version' => $xliffVersion, 'as_tree' => $input->getOption('as-tree'), 'inline' => $input->getOption('as-tree') ?? 0]); - - if (true === $input->getOption('dump-messages')) { - $resultMessage .= ' and translation files were updated'; - } - } - - $io->success($resultMessage.'.'); - - return 0; - } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('locale')) { - $suggestions->suggestValues($this->enabledLocales); - - return; - } - - /** @var KernelInterface $kernel */ - $kernel = $this->getApplication()->getKernel(); - if ($input->mustSuggestArgumentValuesFor('bundle')) { - $bundles = []; - - foreach ($kernel->getBundles() as $bundle) { - $bundles[] = $bundle->getName(); - if ($bundle->getContainerExtension()) { - $bundles[] = $bundle->getContainerExtension()->getAlias(); - } - } - - $suggestions->suggestValues($bundles); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $suggestions->suggestValues(array_merge( - $this->writer->getFormats(), - array_keys(self::FORMATS) - )); - - return; - } - - if ($input->mustSuggestOptionValuesFor('domain') && $locale = $input->getArgument('locale')) { - $extractedCatalogue = $this->extractMessages($locale, $this->getRootCodePaths($kernel), $input->getOption('prefix')); - - $currentCatalogue = $this->loadCurrentMessages($locale, $this->getRootTransPaths()); - - // process catalogues - $operation = $input->getOption('clean') - ? new TargetOperation($currentCatalogue, $extractedCatalogue) - : new MergeOperation($currentCatalogue, $extractedCatalogue); - - $suggestions->suggestValues($operation->getDomains()); - - return; - } - - if ($input->mustSuggestOptionValuesFor('sort')) { - $suggestions->suggestValues(self::SORT_ORDERS); - } - } - - private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue - { - $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); - - // extract intl-icu messages only - $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; - if ($intlMessages = $catalogue->all($intlDomain)) { - $filteredCatalogue->add($intlMessages, $intlDomain); - } - - // extract all messages and subtract intl-icu messages - if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { - $filteredCatalogue->add($messages, $domain); - } - foreach ($catalogue->getResources() as $resource) { - $filteredCatalogue->addResource($resource); - } - - if ($metadata = $catalogue->getMetadata('', $intlDomain)) { - foreach ($metadata as $k => $v) { - $filteredCatalogue->setMetadata($k, $v, $intlDomain); - } - } - - if ($metadata = $catalogue->getMetadata('', $domain)) { - foreach ($metadata as $k => $v) { - $filteredCatalogue->setMetadata($k, $v, $domain); - } - } - - return $filteredCatalogue; - } - - private function sortCatalogue(MessageCatalogue $catalogue, string $sort): MessageCatalogue - { - $sortedCatalogue = new MessageCatalogue($catalogue->getLocale()); - - foreach ($catalogue->getDomains() as $domain) { - // extract intl-icu messages only - $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; - if ($intlMessages = $catalogue->all($intlDomain)) { - if (self::DESC === $sort) { - krsort($intlMessages); - } elseif (self::ASC === $sort) { - ksort($intlMessages); - } - - $sortedCatalogue->add($intlMessages, $intlDomain); - } - - // extract all messages and subtract intl-icu messages - if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { - if (self::DESC === $sort) { - krsort($messages); - } elseif (self::ASC === $sort) { - ksort($messages); - } - - $sortedCatalogue->add($messages, $domain); - } - - if ($metadata = $catalogue->getMetadata('', $intlDomain)) { - foreach ($metadata as $k => $v) { - $sortedCatalogue->setMetadata($k, $v, $intlDomain); - } - } - - if ($metadata = $catalogue->getMetadata('', $domain)) { - foreach ($metadata as $k => $v) { - $sortedCatalogue->setMetadata($k, $v, $domain); - } - } - } - - foreach ($catalogue->getResources() as $resource) { - $sortedCatalogue->addResource($resource); - } - - return $sortedCatalogue; - } - - private function extractMessages(string $locale, array $transPaths, string $prefix): MessageCatalogue - { - $extractedCatalogue = new MessageCatalogue($locale); - $this->extractor->setPrefix($prefix); - $transPaths = $this->filterDuplicateTransPaths($transPaths); - foreach ($transPaths as $path) { - if (is_dir($path) || is_file($path)) { - $this->extractor->extract($path, $extractedCatalogue); - } - } - - return $extractedCatalogue; - } - - private function filterDuplicateTransPaths(array $transPaths): array - { - $transPaths = array_filter(array_map('realpath', $transPaths)); - - sort($transPaths); - - $filteredPaths = []; - - foreach ($transPaths as $path) { - foreach ($filteredPaths as $filteredPath) { - if (str_starts_with($path, $filteredPath.\DIRECTORY_SEPARATOR)) { - continue 2; - } - } - - $filteredPaths[] = $path; - } - - return $filteredPaths; - } - - private function loadCurrentMessages(string $locale, array $transPaths): MessageCatalogue - { - $currentCatalogue = new MessageCatalogue($locale); - foreach ($transPaths as $path) { - if (is_dir($path)) { - $this->reader->read($path, $currentCatalogue); - } - } - - return $currentCatalogue; - } - - private function getRootTransPaths(): array - { - $transPaths = $this->transPaths; - if ($this->defaultTransPath) { - $transPaths[] = $this->defaultTransPath; - } - - return $transPaths; - } - - private function getRootCodePaths(KernelInterface $kernel): array - { - $codePaths = $this->codePaths; - $codePaths[] = $kernel->getProjectDir().'/src'; - if ($this->defaultViewsPath) { - $codePaths[] = $this->defaultViewsPath; - } - - return $codePaths; - } - - private function removeNoFillTranslations(MessageCatalogueInterface $operation): void - { - foreach ($operation->all('messages') as $key => $message) { - if (str_starts_with($message, self::NO_FILL_PREFIX)) { - $operation->set($key, '', 'messages'); - } - } + trigger_deprecation('symfony/framework-bundle', '7.3', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, TranslationExtractCommand::class); + parent::__construct($writer, $reader, $extractor, $defaultLocale, $defaultTransPath, $defaultViewsPath, $transPaths, $codePaths, $enabledLocales); } } diff --git a/Resources/config/console.php b/Resources/config/console.php index 9df82e20e..7168caa4d 100644 --- a/Resources/config/console.php +++ b/Resources/config/console.php @@ -36,7 +36,7 @@ use Symfony\Bundle\FrameworkBundle\Command\SecretsRevealCommand; use Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand; use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand; -use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand; use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; @@ -266,7 +266,7 @@ ]) ->tag('console.command') - ->set('console.command.translation_extract', TranslationUpdateCommand::class) + ->set('console.command.translation_extract', TranslationExtractCommand::class) ->args([ service('translation.writer'), service('translation.reader'), diff --git a/Tests/Command/TranslationUpdateCommandCompletionTest.php b/Tests/Command/TranslationExtractCommandCompletionTest.php similarity index 93% rename from Tests/Command/TranslationUpdateCommandCompletionTest.php rename to Tests/Command/TranslationExtractCommandCompletionTest.php index 4627508cb..6d2f22d96 100644 --- a/Tests/Command/TranslationUpdateCommandCompletionTest.php +++ b/Tests/Command/TranslationExtractCommandCompletionTest.php @@ -12,7 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\DependencyInjection\Container; @@ -25,7 +25,7 @@ use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Writer\TranslationWriter; -class TranslationUpdateCommandCompletionTest extends TestCase +class TranslationExtractCommandCompletionTest extends TestCase { private Filesystem $fs; private string $translationDir; @@ -129,7 +129,7 @@ function ($path, $catalogue) use ($loadedMessages) { ->method('getContainer') ->willReturn($container); - $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']); + $command = new TranslationExtractCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths, ['en', 'fr']); $application = new Application($kernel); $application->add($command); diff --git a/Tests/Command/TranslationUpdateCommandTest.php b/Tests/Command/TranslationExtractCommandTest.php similarity index 96% rename from Tests/Command/TranslationUpdateCommandTest.php rename to Tests/Command/TranslationExtractCommandTest.php index f803c2908..c5e78de12 100644 --- a/Tests/Command/TranslationUpdateCommandTest.php +++ b/Tests/Command/TranslationExtractCommandTest.php @@ -12,7 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; use PHPUnit\Framework\TestCase; -use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; +use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Container; @@ -26,7 +26,7 @@ use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Writer\TranslationWriter; -class TranslationUpdateCommandTest extends TestCase +class TranslationExtractCommandTest extends TestCase { private Filesystem $fs; private string $translationDir; @@ -163,9 +163,9 @@ public function testFilterDuplicateTransPaths() } } - $command = $this->createMock(TranslationUpdateCommand::class); + $command = $this->createMock(TranslationExtractCommand::class); - $method = new \ReflectionMethod(TranslationUpdateCommand::class, 'filterDuplicateTransPaths'); + $method = new \ReflectionMethod(TranslationExtractCommand::class, 'filterDuplicateTransPaths'); $filteredTransPaths = $method->invoke($command, $transPaths); @@ -193,7 +193,7 @@ public function testRemoveNoFillTranslationsMethod($noFillCounter, $messages) ->method('set'); // Calling private method - $translationUpdate = $this->createMock(TranslationUpdateCommand::class); + $translationUpdate = $this->createMock(TranslationExtractCommand::class); $reflection = new \ReflectionObject($translationUpdate); $method = $reflection->getMethod('removeNoFillTranslations'); $method->invokeArgs($translationUpdate, [$operation]); @@ -301,7 +301,7 @@ function (MessageCatalogue $catalogue) use ($writerMessages) { ->method('getContainer') ->willReturn($container); - $command = new TranslationUpdateCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths); + $command = new TranslationExtractCommand($writer, $loader, $extractor, 'en', $this->translationDir.'/translations', $this->translationDir.'/templates', $transPaths, $codePaths); $application = new Application($kernel); $application->add($command); From a2712ce1a8bb200195198c87662886402d8878c6 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 9 Oct 2024 18:56:29 +0200 Subject: [PATCH 003/111] [FrameworkBundle] [JsonEncoder] Wire services --- CHANGELOG.md | 1 + .../Compiler/UnusedTagsPass.php | 3 + DependencyInjection/Configuration.php | 24 ++++ DependencyInjection/FrameworkExtension.php | 46 ++++++- Resources/config/json_encoder.php | 126 ++++++++++++++++++ Resources/config/schema/symfony-1.0.xsd | 10 ++ .../DependencyInjection/ConfigurationTest.php | 4 + .../Fixtures/php/json_encoder.php | 14 ++ .../DependencyInjection/Fixtures/xml/full.xml | 1 + .../Fixtures/xml/json_encoder.xml | 14 ++ .../DependencyInjection/Fixtures/yml/full.yml | 1 + .../Fixtures/yml/json_encoder.yml | 10 ++ .../FrameworkExtensionTestCase.php | 6 + Tests/Functional/JsonEncoderTest.php | 47 +++++++ .../Functional/app/JsonEncoder/Dto/Dummy.php | 32 +++++ .../app/JsonEncoder/RangeNormalizer.php | 38 ++++++ Tests/Functional/app/JsonEncoder/bundles.php | 18 +++ Tests/Functional/app/JsonEncoder/config.yml | 24 ++++ 18 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 Resources/config/json_encoder.php create mode 100644 Tests/DependencyInjection/Fixtures/php/json_encoder.php create mode 100644 Tests/DependencyInjection/Fixtures/xml/json_encoder.xml create mode 100644 Tests/DependencyInjection/Fixtures/yml/json_encoder.yml create mode 100644 Tests/Functional/JsonEncoderTest.php create mode 100644 Tests/Functional/app/JsonEncoder/Dto/Dummy.php create mode 100644 Tests/Functional/app/JsonEncoder/RangeNormalizer.php create mode 100644 Tests/Functional/app/JsonEncoder/bundles.php create mode 100644 Tests/Functional/app/JsonEncoder/config.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f05ad7a5..d63b01723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` + * Add JsonEncoder services and configuration 7.2 --- diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index ae2523e51..45d08a975 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -53,6 +53,9 @@ class UnusedTagsPass implements CompilerPassInterface 'form.type_guesser', 'html_sanitizer', 'http_client.client', + 'json_encoder.denormalizer', + 'json_encoder.encodable', + 'json_encoder.normalizer', 'kernel.cache_clearer', 'kernel.cache_warmer', 'kernel.event_listener', diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 372da4a5e..99592fe49 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -31,6 +31,7 @@ use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\JsonEncoder\EncoderInterface; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; @@ -181,6 +182,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addHtmlSanitizerSection($rootNode, $enableIfStandalone); $this->addWebhookSection($rootNode, $enableIfStandalone); $this->addRemoteEventSection($rootNode, $enableIfStandalone); + $this->addJsonEncoderSection($rootNode, $enableIfStandalone); return $treeBuilder; } @@ -2570,4 +2572,26 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ->end() ; } + + private function addJsonEncoderSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void + { + $rootNode + ->children() + ->arrayNode('json_encoder') + ->info('JSON encoder configuration') + ->{$enableIfStandalone('symfony/json-encoder', EncoderInterface::class)}() + ->fixXmlConfig('path') + ->children() + ->arrayNode('paths') + ->info('Namespaces and paths of encodable/decodable classes.') + ->normalizeKeys(false) + ->useAttributeAsKey('namespace') + ->scalarPrototype()->end() + ->defaultValue([]) + ->end() + ->end() + ->end() + ->end() + ; + } } diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 3f5157582..862abe3ca 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -99,6 +99,11 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; +use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface as JsonEncoderDenormalizerInterface; +use Symfony\Component\JsonEncoder\DecoderInterface as JsonEncoderDecoderInterface; +use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface as JsonEncoderNormalizerInterface; +use Symfony\Component\JsonEncoder\EncoderInterface as JsonEncoderEncoderInterface; +use Symfony\Component\JsonEncoder\JsonEncoder; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\PersistingStoreInterface; @@ -176,6 +181,7 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; +use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Uid\UuidV4; use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; @@ -414,7 +420,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('console.command.serializer_debug'); } - if ($this->readConfigEnabled('type_info', $container, $config['type_info'])) { + if ($typeInfoEnabled = $this->readConfigEnabled('type_info', $container, $config['type_info'])) { $this->registerTypeInfoConfiguration($container, $loader); } @@ -422,6 +428,14 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerPropertyInfoConfiguration($container, $loader); } + if ($this->readConfigEnabled('json_encoder', $container, $config['json_encoder'])) { + if (!$typeInfoEnabled) { + throw new LogicException('JsonEncoder support cannot be enabled as the TypeInfo component is not '.(interface_exists(TypeResolverInterface::class) ? 'enabled.' : 'installed. Try running "composer require symfony/type-info".')); + } + + $this->registerJsonEncoderConfiguration($config['json_encoder'], $container, $loader); + } + if ($this->readConfigEnabled('lock', $container, $config['lock'])) { $this->registerLockConfiguration($config['lock'], $container, $loader); } @@ -1990,6 +2004,36 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []); } + private function registerJsonEncoderConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void + { + if (!class_exists(JsonEncoder::class)) { + throw new LogicException('JsonEncoder support cannot be enabled as the JsonEncoder component is not installed. Try running "composer require symfony/json-encoder".'); + } + + $container->registerForAutoconfiguration(JsonEncoderNormalizerInterface::class) + ->addTag('json_encoder.normalizer'); + $container->registerForAutoconfiguration(JsonEncoderDenormalizerInterface::class) + ->addTag('json_encoder.denormalizer'); + + $loader->load('json_encoder.php'); + + $container->registerAliasForArgument('json_encoder.encoder', JsonEncoderEncoderInterface::class, 'json.encoder'); + $container->registerAliasForArgument('json_encoder.decoder', JsonEncoderDecoderInterface::class, 'json.decoder'); + + $container->setParameter('.json_encoder.encoders_dir', '%kernel.cache_dir%/json_encoder/encoder'); + $container->setParameter('.json_encoder.decoders_dir', '%kernel.cache_dir%/json_encoder/decoder'); + $container->setParameter('.json_encoder.lazy_ghosts_dir', '%kernel.cache_dir%/json_encoder/lazy_ghost'); + + $encodableDefinition = (new Definition()) + ->setAbstract(true) + ->addTag('container.excluded') + ->addTag('json_encoder.encodable'); + + foreach ($config['paths'] as $namespace => $path) { + $loader->registerClasses($encodableDefinition, $namespace, $path); + } + } + private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void { if (!interface_exists(PropertyInfoExtractorInterface::class)) { diff --git a/Resources/config/json_encoder.php b/Resources/config/json_encoder.php new file mode 100644 index 000000000..b864ed0f9 --- /dev/null +++ b/Resources/config/json_encoder.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\JsonEncoder\CacheWarmer\EncoderDecoderCacheWarmer; +use Symfony\Component\JsonEncoder\CacheWarmer\LazyGhostCacheWarmer; +use Symfony\Component\JsonEncoder\Decode\Denormalizer\DateTimeDenormalizer; +use Symfony\Component\JsonEncoder\Encode\Normalizer\DateTimeNormalizer; +use Symfony\Component\JsonEncoder\JsonDecoder; +use Symfony\Component\JsonEncoder\JsonEncoder; +use Symfony\Component\JsonEncoder\Mapping\Decode\AttributePropertyMetadataLoader as DecodeAttributePropertyMetadataLoader; +use Symfony\Component\JsonEncoder\Mapping\Decode\DateTimeTypePropertyMetadataLoader as DecodeDateTimeTypePropertyMetadataLoader; +use Symfony\Component\JsonEncoder\Mapping\Encode\AttributePropertyMetadataLoader as EncodeAttributePropertyMetadataLoader; +use Symfony\Component\JsonEncoder\Mapping\Encode\DateTimeTypePropertyMetadataLoader as EncodeDateTimeTypePropertyMetadataLoader; +use Symfony\Component\JsonEncoder\Mapping\GenericTypePropertyMetadataLoader; +use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoader; + +return static function (ContainerConfigurator $container) { + $container->services() + // encoder/decoder + ->set('json_encoder.encoder', JsonEncoder::class) + ->args([ + tagged_locator('json_encoder.normalizer'), + service('json_encoder.encode.property_metadata_loader'), + param('.json_encoder.encoders_dir'), + false, + ]) + ->set('json_encoder.decoder', JsonDecoder::class) + ->args([ + tagged_locator('json_encoder.denormalizer'), + service('json_encoder.decode.property_metadata_loader'), + param('.json_encoder.decoders_dir'), + param('.json_encoder.lazy_ghosts_dir'), + ]) + ->alias(JsonEncoder::class, 'json_encoder.encoder') + ->alias(JsonDecoder::class, 'json_encoder.decoder') + + // metadata + ->stack('json_encoder.encode.property_metadata_loader', [ + inline_service(EncodeAttributePropertyMetadataLoader::class) + ->args([ + service('.inner'), + tagged_locator('json_encoder.normalizer'), + service('type_info.resolver'), + ]), + inline_service(EncodeDateTimeTypePropertyMetadataLoader::class) + ->args([ + service('.inner'), + ]), + inline_service(GenericTypePropertyMetadataLoader::class) + ->args([ + service('.inner'), + service('type_info.type_context_factory'), + ]), + inline_service(PropertyMetadataLoader::class) + ->args([ + service('type_info.resolver'), + ]), + ]) + + ->stack('json_encoder.decode.property_metadata_loader', [ + inline_service(DecodeAttributePropertyMetadataLoader::class) + ->args([ + service('.inner'), + tagged_locator('json_encoder.denormalizer'), + service('type_info.resolver'), + ]), + inline_service(DecodeDateTimeTypePropertyMetadataLoader::class) + ->args([ + service('.inner'), + ]), + inline_service(GenericTypePropertyMetadataLoader::class) + ->args([ + service('.inner'), + service('type_info.type_context_factory'), + ]), + inline_service(PropertyMetadataLoader::class) + ->args([ + service('type_info.resolver'), + ]), + ]) + + // normalizers/denormalizers + ->set('json_encoder.normalizer.date_time', DateTimeNormalizer::class) + ->tag('json_encoder.normalizer') + ->set('json_encoder.denormalizer.date_time', DateTimeDenormalizer::class) + ->args([ + false, + ]) + ->tag('json_encoder.denormalizer') + ->set('json_encoder.denormalizer.date_time_immutable', DateTimeDenormalizer::class) + ->args([ + true, + ]) + ->tag('json_encoder.denormalizer') + + // cache + ->set('.json_encoder.cache_warmer.encoder_decoder', EncoderDecoderCacheWarmer::class) + ->args([ + tagged_iterator('json_encoder.encodable'), + service('json_encoder.encode.property_metadata_loader'), + service('json_encoder.decode.property_metadata_loader'), + param('.json_encoder.encoders_dir'), + param('.json_encoder.decoders_dir'), + false, + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.cache_warmer') + + ->set('.json_encoder.cache_warmer.lazy_ghost', LazyGhostCacheWarmer::class) + ->args([ + tagged_iterator('json_encoder.encodable'), + param('.json_encoder.lazy_ghosts_dir'), + ]) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 91528b60f..9cb89207d 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -46,6 +46,7 @@ + @@ -1003,4 +1004,13 @@ + + + + + + + + + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index c7113cfb4..963cac638 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -970,6 +970,10 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'remote-event' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(RemoteEvent::class), ], + 'json_encoder' => [ + 'enabled' => false, + 'paths' => [], + ], ]; } diff --git a/Tests/DependencyInjection/Fixtures/php/json_encoder.php b/Tests/DependencyInjection/Fixtures/php/json_encoder.php new file mode 100644 index 000000000..42204b2cb --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/json_encoder.php @@ -0,0 +1,14 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'type_info' => [ + 'enabled' => true, + ], + 'json_encoder' => [ + 'enabled' => true, + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/xml/full.xml b/Tests/DependencyInjection/Fixtures/xml/full.xml index c01e85783..a3e5cfd88 100644 --- a/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -46,5 +46,6 @@ + diff --git a/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml b/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml new file mode 100644 index 000000000..a20f98567 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/yml/full.yml b/Tests/DependencyInjection/Fixtures/yml/full.yml index 7550749eb..8e272d11b 100644 --- a/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -70,3 +70,4 @@ framework: formats: csv: ['text/csv', 'text/plain'] pdf: 'application/pdf' + json_encoder: ~ diff --git a/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml b/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml new file mode 100644 index 000000000..e09f7c7d3 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml @@ -0,0 +1,10 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + type_info: + enabled: true + json_encoder: + enabled: true diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 798217191..0446eb5d2 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -2497,6 +2497,12 @@ public function testSemaphoreWithService() self::assertEquals(new Reference('my_service'), $storeDef->getArgument(0)); } + public function testJsonEncoderEnabled() + { + $container = $this->createContainerFromFile('json_encoder'); + $this->assertTrue($container->has('json_encoder.encoder')); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ diff --git a/Tests/Functional/JsonEncoderTest.php b/Tests/Functional/JsonEncoderTest.php new file mode 100644 index 000000000..0ab66e6c1 --- /dev/null +++ b/Tests/Functional/JsonEncoderTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy; +use Symfony\Component\JsonEncoder\DecoderInterface; +use Symfony\Component\JsonEncoder\EncoderInterface; +use Symfony\Component\TypeInfo\Type; + +/** + * @author Mathias Arlaud + */ +class JsonEncoderTest extends AbstractWebTestCase +{ + public function testEncode() + { + static::bootKernel(['test_case' => 'JsonEncoder']); + + /** @var EncoderInterface $encoder */ + $encoder = static::getContainer()->get('json_encoder.encoder.alias'); + + $this->assertSame('{"@name":"DUMMY","range":"10..20"}', (string) $encoder->encode(new Dummy(), Type::object(Dummy::class))); + } + + public function testDecode() + { + static::bootKernel(['test_case' => 'JsonEncoder']); + + /** @var DecoderInterface $decoder */ + $decoder = static::getContainer()->get('json_encoder.decoder.alias'); + + $expected = new Dummy(); + $expected->name = 'dummy'; + $expected->range = [0, 1]; + + $this->assertEquals($expected, $decoder->decode('{"@name": "DUMMY", "range": "0..1"}', Type::object(Dummy::class))); + } +} diff --git a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php new file mode 100644 index 000000000..344b9d11c --- /dev/null +++ b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto; + +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer; +use Symfony\Component\JsonEncoder\Attribute\Denormalizer; +use Symfony\Component\JsonEncoder\Attribute\EncodedName; +use Symfony\Component\JsonEncoder\Attribute\Normalizer; + +/** + * @author Mathias Arlaud + */ +class Dummy +{ + #[EncodedName('@name')] + #[Normalizer('strtoupper')] + #[Denormalizer('strtolower')] + public string $name = 'dummy'; + + #[Normalizer(RangeNormalizer::class)] + #[Denormalizer(RangeNormalizer::class)] + public array $range = [10, 20]; +} diff --git a/Tests/Functional/app/JsonEncoder/RangeNormalizer.php b/Tests/Functional/app/JsonEncoder/RangeNormalizer.php new file mode 100644 index 000000000..beb9e8188 --- /dev/null +++ b/Tests/Functional/app/JsonEncoder/RangeNormalizer.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder; + +use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface; +use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; + +/** + * @author Mathias Arlaud + */ +class RangeNormalizer implements NormalizerInterface, DenormalizerInterface +{ + public function normalize(mixed $denormalized, array $options = []): string + { + return $denormalized[0].'..'.$denormalized[1]; + } + + public function denormalize(mixed $normalized, array $options = []): array + { + return array_map(static fn (string $v): int => (int) $v, explode('..', $normalized)); + } + + public static function getNormalizedType(): BuiltinType + { + return Type::string(); + } +} diff --git a/Tests/Functional/app/JsonEncoder/bundles.php b/Tests/Functional/app/JsonEncoder/bundles.php new file mode 100644 index 000000000..15ff182c6 --- /dev/null +++ b/Tests/Functional/app/JsonEncoder/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; + +return [ + new FrameworkBundle(), + new TestBundle(), +]; diff --git a/Tests/Functional/app/JsonEncoder/config.yml b/Tests/Functional/app/JsonEncoder/config.yml new file mode 100644 index 000000000..55fdf53f5 --- /dev/null +++ b/Tests/Functional/app/JsonEncoder/config.yml @@ -0,0 +1,24 @@ +imports: + - { resource: ../config/default.yml } + +framework: + http_method_override: false + type_info: ~ + json_encoder: + enabled: true + paths: + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\: '../../Tests/Functional/app/JsonEncoder/Dto/*' + +services: + _defaults: + autoconfigure: true + + json_encoder.encoder.alias: + alias: json_encoder.encoder + public: true + + json_encoder.decoder.alias: + alias: json_encoder.decoder + public: true + + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer: ~ From 4bf82367c2efedec338d102ebb1b4e6938b040de Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 11 Dec 2024 13:53:11 +0100 Subject: [PATCH 004/111] [FrameworkBundle] Fix `JsonEncoder` config on low-deps --- Tests/DependencyInjection/ConfigurationTest.php | 3 ++- composer.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 963cac638..b4b8eb875 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\JsonEncoder\JsonEncoder; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\MessageBusInterface; @@ -971,7 +972,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'enabled' => !class_exists(FullStack::class) && class_exists(RemoteEvent::class), ], 'json_encoder' => [ - 'enabled' => false, + 'enabled' => !class_exists(FullStack::class) && class_exists(JsonEncoder::class), 'paths' => [], ], ]; diff --git a/composer.json b/composer.json index 9b3e7c86e..81dade063 100644 --- a/composer.json +++ b/composer.json @@ -69,6 +69,7 @@ "symfony/workflow": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", + "symfony/json-encoder": "^7.3", "symfony/uid": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0", "symfony/webhook": "^7.2", From d22981663bd17c9d2fa710cdd8cdc464ed8f47be Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 11 Dec 2024 16:32:39 +0100 Subject: [PATCH 005/111] [JsonEncoder] [FrameworkBundle] Fix service definition --- Resources/config/json_encoder.php | 86 ++++++++++++++++--------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/Resources/config/json_encoder.php b/Resources/config/json_encoder.php index b864ed0f9..421f10c9a 100644 --- a/Resources/config/json_encoder.php +++ b/Resources/config/json_encoder.php @@ -45,49 +45,51 @@ ->alias(JsonDecoder::class, 'json_encoder.decoder') // metadata - ->stack('json_encoder.encode.property_metadata_loader', [ - inline_service(EncodeAttributePropertyMetadataLoader::class) - ->args([ - service('.inner'), - tagged_locator('json_encoder.normalizer'), - service('type_info.resolver'), - ]), - inline_service(EncodeDateTimeTypePropertyMetadataLoader::class) - ->args([ - service('.inner'), - ]), - inline_service(GenericTypePropertyMetadataLoader::class) - ->args([ - service('.inner'), - service('type_info.type_context_factory'), - ]), - inline_service(PropertyMetadataLoader::class) - ->args([ - service('type_info.resolver'), - ]), - ]) + ->set('json_encoder.encode.property_metadata_loader', PropertyMetadataLoader::class) + ->args([ + service('type_info.resolver'), + ]) + ->set('.json_encoder.encode.property_metadata_loader.generic', GenericTypePropertyMetadataLoader::class) + ->decorate('json_encoder.encode.property_metadata_loader') + ->args([ + service('.inner'), + service('type_info.type_context_factory'), + ]) + ->set('.json_encoder.encode.property_metadata_loader.date_time', EncodeDateTimeTypePropertyMetadataLoader::class) + ->decorate('json_encoder.encode.property_metadata_loader') + ->args([ + service('.inner'), + ]) + ->set('.json_encoder.encode.property_metadata_loader.attribute', EncodeAttributePropertyMetadataLoader::class) + ->decorate('json_encoder.encode.property_metadata_loader') + ->args([ + service('.inner'), + tagged_locator('json_encoder.normalizer'), + service('type_info.resolver'), + ]) - ->stack('json_encoder.decode.property_metadata_loader', [ - inline_service(DecodeAttributePropertyMetadataLoader::class) - ->args([ - service('.inner'), - tagged_locator('json_encoder.denormalizer'), - service('type_info.resolver'), - ]), - inline_service(DecodeDateTimeTypePropertyMetadataLoader::class) - ->args([ - service('.inner'), - ]), - inline_service(GenericTypePropertyMetadataLoader::class) - ->args([ - service('.inner'), - service('type_info.type_context_factory'), - ]), - inline_service(PropertyMetadataLoader::class) - ->args([ - service('type_info.resolver'), - ]), - ]) + ->set('json_encoder.decode.property_metadata_loader', PropertyMetadataLoader::class) + ->args([ + service('type_info.resolver'), + ]) + ->set('.json_encoder.decode.property_metadata_loader.generic', GenericTypePropertyMetadataLoader::class) + ->decorate('json_encoder.decode.property_metadata_loader') + ->args([ + service('.inner'), + service('type_info.type_context_factory'), + ]) + ->set('.json_encoder.decode.property_metadata_loader.date_time', DecodeDateTimeTypePropertyMetadataLoader::class) + ->decorate('json_encoder.decode.property_metadata_loader') + ->args([ + service('.inner'), + ]) + ->set('.json_encoder.decode.property_metadata_loader.attribute', DecodeAttributePropertyMetadataLoader::class) + ->decorate('json_encoder.decode.property_metadata_loader') + ->args([ + service('.inner'), + tagged_locator('json_encoder.normalizer'), + service('type_info.resolver'), + ]) // normalizers/denormalizers ->set('json_encoder.normalizer.date_time', DateTimeNormalizer::class) From b77eac006b9314ecb76d63023429b707105fe760 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 11 Dec 2024 13:35:15 +0100 Subject: [PATCH 006/111] [JsonEncoder] Add native lazyghost support --- DependencyInjection/FrameworkExtension.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 862abe3ca..d911b767d 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2032,6 +2032,10 @@ private function registerJsonEncoderConfiguration(array $config, ContainerBuilde foreach ($config['paths'] as $namespace => $path) { $loader->registerClasses($encodableDefinition, $namespace, $path); } + + if (\PHP_VERSION_ID >= 80400) { + $container->removeDefinition('.json_encoder.cache_warmer.lazy_ghost'); + } } private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void From bea1598816d20bd6a3b9ae92a80b4e793697854e Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 11 Dec 2024 15:33:47 +0100 Subject: [PATCH 007/111] [FrameworkBundle] Fix `symfony/translation` conflict --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 81dade063..afaa9b03b 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "symfony/serializer": "^7.1", "symfony/stopwatch": "^6.4|^7.0", "symfony/string": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0", "symfony/twig-bundle": "^6.4|^7.0", "symfony/type-info": "^7.1", "symfony/validator": "^6.4|^7.0", @@ -100,7 +100,7 @@ "symfony/security-core": "<6.4", "symfony/serializer": "<7.1", "symfony/stopwatch": "<6.4", - "symfony/translation": "<6.4", + "symfony/translation": "<6.4.3", "symfony/twig-bridge": "<6.4", "symfony/twig-bundle": "<6.4", "symfony/validator": "<6.4", From 8eb6543e74ff33e1e268867ebc931aae1aef8dea Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 008/111] chore: PHP CS Fixer fixes --- Command/AboutCommand.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Command/AboutCommand.php b/Command/AboutCommand.php index 4dc86130a..0c6899328 100644 --- a/Command/AboutCommand.php +++ b/Command/AboutCommand.php @@ -84,9 +84,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int ['Architecture', (\PHP_INT_SIZE * 8).' bits'], ['Intl locale', class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a'], ['Timezone', date_default_timezone_get().' ('.(new \DateTimeImmutable())->format(\DateTimeInterface::W3C).')'], - ['OPcache', \extension_loaded('Zend OPcache') ? (filter_var(\ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed'], - ['APCu', \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed'], - ['Xdebug', \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled (' . $xdebugMode . ')' : 'Not enabled') : 'Not installed'], + ['OPcache', \extension_loaded('Zend OPcache') ? (filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed'], + ['APCu', \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed'], + ['Xdebug', \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled ('.$xdebugMode.')' : 'Not enabled') : 'Not installed'], ]; $io->table([], $rows); From 06e1348872bae94fff34a6f231822b3cf697f492 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 11 Dec 2024 16:44:21 +0100 Subject: [PATCH 009/111] do not allow symfony/json-encoder 7.4 yet The component is experimental. Claiming to be compatible with 7.4 could lead to having to change a stable 7.3 release of the FrameworkBundle if the JsonEncoder component introduced BC breaks in 7.4. --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index afaa9b03b..a7673d9f7 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "symfony/workflow": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", - "symfony/json-encoder": "^7.3", + "symfony/json-encoder": "7.3.*", "symfony/uid": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0", "symfony/webhook": "^7.2", @@ -88,6 +88,7 @@ "symfony/dom-crawler": "<6.4", "symfony/http-client": "<6.4", "symfony/form": "<6.4", + "symfony/json-encoder": ">=7.4", "symfony/lock": "<6.4", "symfony/mailer": "<6.4", "symfony/messenger": "<6.4", From 7bb5613289191d80a8e1e60b9eced2e83edfc6ef Mon Sep 17 00:00:00 2001 From: valtzu Date: Wed, 27 Nov 2024 22:28:12 +0200 Subject: [PATCH 010/111] Generate url-safe signatures --- Tests/Functional/FragmentTest.php | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Functional/FragmentTest.php b/Tests/Functional/FragmentTest.php index 6d8966a17..b530d2cbc 100644 --- a/Tests/Functional/FragmentTest.php +++ b/Tests/Functional/FragmentTest.php @@ -50,6 +50,6 @@ public function testGenerateFragmentUri() $client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]); $client->request('GET', '/fragment_uri'); - $this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent()); + $this->assertSame('/_fragment?_hash=CCRGN2D_oAJbeGz__doH3bNSPwLCrmwC1zAYCGIKJ0E&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent()); } } diff --git a/composer.json b/composer.json index afaa9b03b..ef669713f 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-foundation": "^7.3", "symfony/http-kernel": "^7.2", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "^7.1", From bc39c5b05c9944dbe1d4dfd50419828bffdafccf Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Sat, 7 Dec 2024 10:46:53 +0100 Subject: [PATCH 011/111] [JsonEncoder] Remove chunk size definition --- Resources/config/json_encoder.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Resources/config/json_encoder.php b/Resources/config/json_encoder.php index 421f10c9a..24f596fd4 100644 --- a/Resources/config/json_encoder.php +++ b/Resources/config/json_encoder.php @@ -32,7 +32,6 @@ tagged_locator('json_encoder.normalizer'), service('json_encoder.encode.property_metadata_loader'), param('.json_encoder.encoders_dir'), - false, ]) ->set('json_encoder.decoder', JsonDecoder::class) ->args([ @@ -113,7 +112,6 @@ service('json_encoder.decode.property_metadata_loader'), param('.json_encoder.encoders_dir'), param('.json_encoder.decoders_dir'), - false, service('logger')->ignoreOnInvalid(), ]) ->tag('kernel.cache_warmer') From 6623abcaabbf97fbac9f7da993f4cf148d557e94 Mon Sep 17 00:00:00 2001 From: Farhad Hedayatifard Date: Mon, 28 Oct 2024 22:24:40 +0330 Subject: [PATCH 012/111] [Mailer] Add AhaSend Bridge --- DependencyInjection/FrameworkExtension.php | 2 ++ Resources/config/mailer_transports.php | 2 ++ Resources/config/mailer_webhook.php | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index a7749cd30..0c87c4aea 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2670,6 +2670,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } $classToServices = [ + MailerBridge\AhaSend\Transport\AhaSendTransportFactory::class => 'mailer.transport_factory.ahasend', MailerBridge\Azure\Transport\AzureTransportFactory::class => 'mailer.transport_factory.azure', MailerBridge\Brevo\Transport\BrevoTransportFactory::class => 'mailer.transport_factory.brevo', MailerBridge\Google\Transport\GmailTransportFactory::class => 'mailer.transport_factory.gmail', @@ -2700,6 +2701,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co if ($webhookEnabled) { $webhookRequestParsers = [ + MailerBridge\AhaSend\Webhook\AhaSendRequestParser::class => 'mailer.webhook.request_parser.ahasend', MailerBridge\Brevo\Webhook\BrevoRequestParser::class => 'mailer.webhook.request_parser.brevo', MailerBridge\MailerSend\Webhook\MailerSendRequestParser::class => 'mailer.webhook.request_parser.mailersend', MailerBridge\Mailchimp\Webhook\MailchimpRequestParser::class => 'mailer.webhook.request_parser.mailchimp', diff --git a/Resources/config/mailer_transports.php b/Resources/config/mailer_transports.php index c0e7cc06a..2c79b4d55 100644 --- a/Resources/config/mailer_transports.php +++ b/Resources/config/mailer_transports.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Mailer\Bridge\AhaSend\Transport\AhaSendTransportFactory; use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; use Symfony\Component\Mailer\Bridge\Azure\Transport\AzureTransportFactory; use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; @@ -47,6 +48,7 @@ ->tag('monolog.logger', ['channel' => 'mailer']); $factories = [ + 'ahasend' => AhaSendTransportFactory::class, 'amazon' => SesTransportFactory::class, 'azure' => AzureTransportFactory::class, 'brevo' => BrevoTransportFactory::class, diff --git a/Resources/config/mailer_webhook.php b/Resources/config/mailer_webhook.php index c574324db..b815336b2 100644 --- a/Resources/config/mailer_webhook.php +++ b/Resources/config/mailer_webhook.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Mailer\Bridge\AhaSend\RemoteEvent\AhaSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\AhaSend\Webhook\AhaSendRequestParser; use Symfony\Component\Mailer\Bridge\Brevo\RemoteEvent\BrevoPayloadConverter; use Symfony\Component\Mailer\Bridge\Brevo\Webhook\BrevoRequestParser; use Symfony\Component\Mailer\Bridge\Mailchimp\RemoteEvent\MailchimpPayloadConverter; @@ -86,6 +88,11 @@ ->args([service('mailer.payload_converter.sweego')]) ->alias(SweegoRequestParser::class, 'mailer.webhook.request_parser.sweego') + ->set('mailer.payload_converter.ahasend', AhaSendPayloadConverter::class) + ->set('mailer.webhook.request_parser.ahasend', AhaSendRequestParser::class) + ->args([service('mailer.payload_converter.ahasend')]) + ->alias(AhaSendRequestParser::class, 'mailer.webhook.request_parser.ahasend') + ->set('mailer.payload_converter.mailchimp', MailchimpPayloadConverter::class) ->set('mailer.webhook.request_parser.mailchimp', MailchimpRequestParser::class) ->args([service('mailer.payload_converter.mailchimp')]) From b36a93239efa717c70b918681c4baf3ed86452b2 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Sun, 5 Jan 2025 20:49:23 +0100 Subject: [PATCH 013/111] [Security] OAuth2 Introspection Endpoint (RFC7662) In addition to the excellent work of @vincentchalamon #48272, this PR allows getting the data from the OAuth2 Introspection Endpoint. This endpoint is defined in the [RFC7662](https://datatracker.ietf.org/doc/html/rfc7662). It returns the following information that is used to retrieve the user: * If the access token is active * A set of claims that are similar to the OIDC one, including the `sub` or the `username`. --- DependencyInjection/Compiler/UnusedTagsPass.php | 1 + 1 file changed, 1 insertion(+) diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 45d08a975..c135538c2 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -85,6 +85,7 @@ class UnusedTagsPass implements CompilerPassInterface 'routing.route_loader', 'scheduler.schedule_provider', 'scheduler.task', + 'security.access_token_handler.oidc.encryption_algorithm', 'security.access_token_handler.oidc.signature_algorithm', 'security.authenticator.login_linker', 'security.expression_language_provider', From b67010785788ca19137b05e176fcf8530bad7358 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sat, 27 May 2023 16:13:37 +0200 Subject: [PATCH 014/111] [FrameworkBundle][PropertyInfo] Wire the `ConstructorExtractor` class --- CHANGELOG.md | 1 + DependencyInjection/Compiler/UnusedTagsPass.php | 1 + DependencyInjection/Configuration.php | 15 +++++++++++++++ DependencyInjection/FrameworkExtension.php | 13 +++++++++++-- FrameworkBundle.php | 2 ++ Resources/config/property_info.php | 6 ++++++ Resources/config/schema/symfony-1.0.xsd | 1 + Tests/DependencyInjection/ConfigurationTest.php | 2 +- Tests/DependencyInjection/Fixtures/php/full.php | 5 ++++- .../Fixtures/php/property_info.php | 1 + .../property_info_with_constructor_extractor.php | 12 ++++++++++++ .../Fixtures/php/validation_auto_mapping.php | 5 ++++- Tests/DependencyInjection/Fixtures/xml/full.xml | 2 +- .../Fixtures/xml/property_info.xml | 2 +- .../property_info_with_constructor_extractor.xml | 13 +++++++++++++ .../Fixtures/xml/validation_auto_mapping.xml | 2 +- Tests/DependencyInjection/Fixtures/yml/full.yml | 3 ++- .../Fixtures/yml/property_info.yml | 1 + .../property_info_with_constructor_extractor.yml | 9 +++++++++ .../Fixtures/yml/validation_auto_mapping.yml | 4 +++- .../FrameworkExtensionTestCase.php | 8 ++++++++ Tests/Functional/app/ApiAttributesTest/config.yml | 4 +++- Tests/Functional/app/ContainerDump/config.yml | 4 +++- Tests/Functional/app/Serializer/config.yml | 4 +++- 24 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 Tests/DependencyInjection/Fixtures/php/property_info_with_constructor_extractor.php create mode 100644 Tests/DependencyInjection/Fixtures/xml/property_info_with_constructor_extractor.xml create mode 100644 Tests/DependencyInjection/Fixtures/yml/property_info_with_constructor_extractor.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index d63b01723..02bfe6af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` * Add JsonEncoder services and configuration + * Add new `framework.property_info.with_constructor_extractor` option to allow enabling or disabling the constructor extractor integration 7.2 --- diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 45d08a975..a2a571f83 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -73,6 +73,7 @@ class UnusedTagsPass implements CompilerPassInterface 'monolog.logger', 'notifier.channel', 'property_info.access_extractor', + 'property_info.constructor_extractor', 'property_info.initializable_extractor', 'property_info.list_extractor', 'property_info.type_extractor', diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 99592fe49..cbfe657a6 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1226,8 +1226,23 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable ->arrayNode('property_info') ->info('Property info configuration') ->{$enableIfStandalone('symfony/property-info', PropertyInfoExtractorInterface::class)}() + ->children() + ->booleanNode('with_constructor_extractor') + ->info('Registers the constructor extractor.') + ->end() + ->end() ->end() ->end() + ->validate() + ->ifTrue(fn ($v) => $v['property_info']['enabled'] && !isset($v['property_info']['with_constructor_extractor'])) + ->then(function ($v) { + $v['property_info']['with_constructor_extractor'] = false; + + trigger_deprecation('symfony/property-info', '7.3', 'Not setting the "with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); + + return $v; + }) + ->end() ; } diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index eb0838228..5245e8597 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -135,6 +135,7 @@ use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface; use Symfony\Component\Process\Messenger\RunProcessMessageHandler; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; @@ -427,7 +428,7 @@ public function load(array $configs, ContainerBuilder $container): void } if ($propertyInfoEnabled) { - $this->registerPropertyInfoConfiguration($container, $loader); + $this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader); } if ($this->readConfigEnabled('json_encoder', $container, $config['json_encoder'])) { @@ -657,6 +658,8 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('property_info.list_extractor'); $container->registerForAutoconfiguration(PropertyTypeExtractorInterface::class) ->addTag('property_info.type_extractor'); + $container->registerForAutoconfiguration(ConstructorArgumentTypeExtractorInterface::class) + ->addTag('property_info.constructor_extractor'); $container->registerForAutoconfiguration(PropertyDescriptionExtractorInterface::class) ->addTag('property_info.description_extractor'); $container->registerForAutoconfiguration(PropertyAccessExtractorInterface::class) @@ -2040,7 +2043,7 @@ private function registerJsonEncoderConfiguration(array $config, ContainerBuilde } } - private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void + private function registerPropertyInfoConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void { if (!interface_exists(PropertyInfoExtractorInterface::class)) { throw new LogicException('PropertyInfo support cannot be enabled as the PropertyInfo component is not installed. Try running "composer require symfony/property-info".'); @@ -2048,18 +2051,24 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, $loader->load('property_info.php'); + if (!$config['with_constructor_extractor']) { + $container->removeDefinition('property_info.constructor_extractor'); + } + if ( ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/property-info']) && ContainerBuilder::willBeAvailable('phpdocumentor/type-resolver', ContextFactory::class, ['symfony/framework-bundle', 'symfony/property-info']) ) { $definition = $container->register('property_info.phpstan_extractor', PhpStanExtractor::class); $definition->addTag('property_info.type_extractor', ['priority' => -1000]); + $definition->addTag('property_info.constructor_extractor', ['priority' => -1000]); } if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'], true)) { $definition = $container->register('property_info.php_doc_extractor', PhpDocExtractor::class); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); $definition->addTag('property_info.type_extractor', ['priority' => -1001]); + $definition->addTag('property_info.constructor_extractor', ['priority' => -1001]); } if ($container->getParameter('kernel.debug')) { diff --git a/FrameworkBundle.php b/FrameworkBundle.php index e83c4dfe6..ecd0fe682 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -56,6 +56,7 @@ use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\DependencyInjection\MessengerPass; use Symfony\Component\Mime\DependencyInjection\AddMimeTypeGuesserPass; +use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass; use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass; use Symfony\Component\Routing\DependencyInjection\AddExpressionLanguageProvidersPass; use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass; @@ -164,6 +165,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new FragmentRendererPass()); $this->addCompilerPassIfExists($container, SerializerPass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); + $this->addCompilerPassIfExists($container, PropertyInfoConstructorPass::class); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); $container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32); $container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/Resources/config/property_info.php b/Resources/config/property_info.php index 90587839d..f45d6ce2b 100644 --- a/Resources/config/property_info.php +++ b/Resources/config/property_info.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface; @@ -43,9 +44,14 @@ ->set('property_info.reflection_extractor', ReflectionExtractor::class) ->tag('property_info.list_extractor', ['priority' => -1000]) ->tag('property_info.type_extractor', ['priority' => -1002]) + ->tag('property_info.constructor_extractor', ['priority' => -1002]) ->tag('property_info.access_extractor', ['priority' => -1000]) ->tag('property_info.initializable_extractor', ['priority' => -1000]) + ->set('property_info.constructor_extractor', ConstructorExtractor::class) + ->args([[]]) + ->tag('property_info.type_extractor', ['priority' => -999]) + ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') ; diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 9cb89207d..b44f1ccc4 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -370,6 +370,7 @@ + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index b4b8eb875..f1e88b11b 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -808,7 +808,7 @@ protected static function getBundleDefaultConfig() ], 'property_info' => [ 'enabled' => !class_exists(FullStack::class), - ], + ] + (!class_exists(FullStack::class) ? ['with_constructor_extractor' => false] : []), 'router' => [ 'enabled' => false, 'default_uri' => null, diff --git a/Tests/DependencyInjection/Fixtures/php/full.php b/Tests/DependencyInjection/Fixtures/php/full.php index 0a32ce8b3..cb7762829 100644 --- a/Tests/DependencyInjection/Fixtures/php/full.php +++ b/Tests/DependencyInjection/Fixtures/php/full.php @@ -74,7 +74,10 @@ ], ], ], - 'property_info' => true, + 'property_info' => [ + 'enabled' => true, + 'with_constructor_extractor' => true, + ], 'type_info' => true, 'ide' => 'file%%link%%format', 'request' => [ diff --git a/Tests/DependencyInjection/Fixtures/php/property_info.php b/Tests/DependencyInjection/Fixtures/php/property_info.php index b234c4527..e2437e2c2 100644 --- a/Tests/DependencyInjection/Fixtures/php/property_info.php +++ b/Tests/DependencyInjection/Fixtures/php/property_info.php @@ -7,5 +7,6 @@ 'php_errors' => ['log' => true], 'property_info' => [ 'enabled' => true, + 'with_constructor_extractor' => false, ], ]); diff --git a/Tests/DependencyInjection/Fixtures/php/property_info_with_constructor_extractor.php b/Tests/DependencyInjection/Fixtures/php/property_info_with_constructor_extractor.php new file mode 100644 index 000000000..fa143d2e1 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/property_info_with_constructor_extractor.php @@ -0,0 +1,12 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'property_info' => [ + 'enabled' => true, + 'with_constructor_extractor' => true, + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/validation_auto_mapping.php b/Tests/DependencyInjection/Fixtures/php/validation_auto_mapping.php index ae5bea2ea..67bac4a32 100644 --- a/Tests/DependencyInjection/Fixtures/php/validation_auto_mapping.php +++ b/Tests/DependencyInjection/Fixtures/php/validation_auto_mapping.php @@ -5,7 +5,10 @@ 'http_method_override' => false, 'handle_all_throwables' => true, 'php_errors' => ['log' => true], - 'property_info' => ['enabled' => true], + 'property_info' => [ + 'enabled' => true, + 'with_constructor_extractor' => true, + ], 'validation' => [ 'email_validation_mode' => 'html5', 'auto_mapping' => [ diff --git a/Tests/DependencyInjection/Fixtures/xml/full.xml b/Tests/DependencyInjection/Fixtures/xml/full.xml index a3e5cfd88..23d325e61 100644 --- a/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -44,7 +44,7 @@ - + diff --git a/Tests/DependencyInjection/Fixtures/xml/property_info.xml b/Tests/DependencyInjection/Fixtures/xml/property_info.xml index 5f49aabaa..19bac44d9 100644 --- a/Tests/DependencyInjection/Fixtures/xml/property_info.xml +++ b/Tests/DependencyInjection/Fixtures/xml/property_info.xml @@ -8,6 +8,6 @@ - + diff --git a/Tests/DependencyInjection/Fixtures/xml/property_info_with_constructor_extractor.xml b/Tests/DependencyInjection/Fixtures/xml/property_info_with_constructor_extractor.xml new file mode 100644 index 000000000..df8dabe0b --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/property_info_with_constructor_extractor.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/xml/validation_auto_mapping.xml b/Tests/DependencyInjection/Fixtures/xml/validation_auto_mapping.xml index c60691b0b..966598091 100644 --- a/Tests/DependencyInjection/Fixtures/xml/validation_auto_mapping.xml +++ b/Tests/DependencyInjection/Fixtures/xml/validation_auto_mapping.xml @@ -6,7 +6,7 @@ - + foo diff --git a/Tests/DependencyInjection/Fixtures/yml/full.yml b/Tests/DependencyInjection/Fixtures/yml/full.yml index 8e272d11b..28c4336d9 100644 --- a/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -64,7 +64,8 @@ framework: default_context: enable_max_depth: false type_info: ~ - property_info: ~ + property_info: + with_constructor_extractor: true ide: file%%link%%format request: formats: diff --git a/Tests/DependencyInjection/Fixtures/yml/property_info.yml b/Tests/DependencyInjection/Fixtures/yml/property_info.yml index de05e6bb7..4fde73710 100644 --- a/Tests/DependencyInjection/Fixtures/yml/property_info.yml +++ b/Tests/DependencyInjection/Fixtures/yml/property_info.yml @@ -6,3 +6,4 @@ framework: log: true property_info: enabled: true + with_constructor_extractor: false diff --git a/Tests/DependencyInjection/Fixtures/yml/property_info_with_constructor_extractor.yml b/Tests/DependencyInjection/Fixtures/yml/property_info_with_constructor_extractor.yml new file mode 100644 index 000000000..a43762df3 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/property_info_with_constructor_extractor.yml @@ -0,0 +1,9 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + property_info: + enabled: true + with_constructor_extractor: true diff --git a/Tests/DependencyInjection/Fixtures/yml/validation_auto_mapping.yml b/Tests/DependencyInjection/Fixtures/yml/validation_auto_mapping.yml index 55a43886f..e81203e24 100644 --- a/Tests/DependencyInjection/Fixtures/yml/validation_auto_mapping.yml +++ b/Tests/DependencyInjection/Fixtures/yml/validation_auto_mapping.yml @@ -4,7 +4,9 @@ framework: handle_all_throwables: true php_errors: log: true - property_info: { enabled: true } + property_info: + enabled: true + with_constructor_extractor: true validation: email_validation_mode: html5 auto_mapping: diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 0446eb5d2..f5c93cefd 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1676,6 +1676,14 @@ public function testPropertyInfoEnabled() { $container = $this->createContainerFromFile('property_info'); $this->assertTrue($container->has('property_info')); + $this->assertFalse($container->has('property_info.constructor_extractor')); + } + + public function testPropertyInfoWithConstructorExtractorEnabled() + { + $container = $this->createContainerFromFile('property_info_with_constructor_extractor'); + $this->assertTrue($container->has('property_info')); + $this->assertTrue($container->has('property_info.constructor_extractor')); } public function testPropertyInfoCacheActivated() diff --git a/Tests/Functional/app/ApiAttributesTest/config.yml b/Tests/Functional/app/ApiAttributesTest/config.yml index 8b218d48c..00bdd8ab9 100644 --- a/Tests/Functional/app/ApiAttributesTest/config.yml +++ b/Tests/Functional/app/ApiAttributesTest/config.yml @@ -5,4 +5,6 @@ framework: serializer: enabled: true validation: true - property_info: { enabled: true } + property_info: + enabled: true + with_constructor_extractor: true diff --git a/Tests/Functional/app/ContainerDump/config.yml b/Tests/Functional/app/ContainerDump/config.yml index 3efa5f950..48bff3240 100644 --- a/Tests/Functional/app/ContainerDump/config.yml +++ b/Tests/Functional/app/ContainerDump/config.yml @@ -15,6 +15,8 @@ framework: translator: true validation: true serializer: true - property_info: true + property_info: + enabled: true + with_constructor_extractor: true csrf_protection: true form: true diff --git a/Tests/Functional/app/Serializer/config.yml b/Tests/Functional/app/Serializer/config.yml index 2f20dab9e..3c0c35417 100644 --- a/Tests/Functional/app/Serializer/config.yml +++ b/Tests/Functional/app/Serializer/config.yml @@ -10,7 +10,9 @@ framework: max_depth_handler: Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Serializer\MaxDepthHandler default_context: enable_max_depth: true - property_info: { enabled: true } + property_info: + enabled: true + with_constructor_extractor: true services: serializer.alias: From c20716a0ab4907729974a20c182a188a0c9b138c Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 23 Dec 2024 11:21:00 +0100 Subject: [PATCH 015/111] [JsonEncoder] Fix retrieving encodable classes --- DependencyInjection/Configuration.php | 10 ---------- DependencyInjection/FrameworkExtension.php | 9 --------- FrameworkBundle.php | 2 ++ Resources/config/json_encoder.php | 4 ++-- Resources/config/schema/symfony-1.0.xsd | 6 +----- Tests/DependencyInjection/ConfigurationTest.php | 1 - Tests/Functional/app/JsonEncoder/config.yml | 5 +---- 7 files changed, 6 insertions(+), 31 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 99592fe49..7f97199f7 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2580,16 +2580,6 @@ private function addJsonEncoderSection(ArrayNodeDefinition $rootNode, callable $ ->arrayNode('json_encoder') ->info('JSON encoder configuration') ->{$enableIfStandalone('symfony/json-encoder', EncoderInterface::class)}() - ->fixXmlConfig('path') - ->children() - ->arrayNode('paths') - ->info('Namespaces and paths of encodable/decodable classes.') - ->normalizeKeys(false) - ->useAttributeAsKey('namespace') - ->scalarPrototype()->end() - ->defaultValue([]) - ->end() - ->end() ->end() ->end() ; diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 3116116f2..7b40e1cfa 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2026,15 +2026,6 @@ private function registerJsonEncoderConfiguration(array $config, ContainerBuilde $container->setParameter('.json_encoder.decoders_dir', '%kernel.cache_dir%/json_encoder/decoder'); $container->setParameter('.json_encoder.lazy_ghosts_dir', '%kernel.cache_dir%/json_encoder/lazy_ghost'); - $encodableDefinition = (new Definition()) - ->setAbstract(true) - ->addTag('container.excluded') - ->addTag('json_encoder.encodable'); - - foreach ($config['paths'] as $namespace => $path) { - $loader->registerClasses($encodableDefinition, $namespace, $path); - } - if (\PHP_VERSION_ID >= 80400) { $container->removeDefinition('.json_encoder.cache_warmer.lazy_ghost'); } diff --git a/FrameworkBundle.php b/FrameworkBundle.php index e83c4dfe6..843d3e00e 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -54,6 +54,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\JsonEncoder\DependencyInjection\EncodablePass; use Symfony\Component\Messenger\DependencyInjection\MessengerPass; use Symfony\Component\Mime\DependencyInjection\AddMimeTypeGuesserPass; use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass; @@ -186,6 +187,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new VirtualRequestStackPass()); $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass(new EncodablePass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/Resources/config/json_encoder.php b/Resources/config/json_encoder.php index 24f596fd4..67cb25d0a 100644 --- a/Resources/config/json_encoder.php +++ b/Resources/config/json_encoder.php @@ -107,7 +107,7 @@ // cache ->set('.json_encoder.cache_warmer.encoder_decoder', EncoderDecoderCacheWarmer::class) ->args([ - tagged_iterator('json_encoder.encodable'), + abstract_arg('encodable class names'), service('json_encoder.encode.property_metadata_loader'), service('json_encoder.decode.property_metadata_loader'), param('.json_encoder.encoders_dir'), @@ -118,7 +118,7 @@ ->set('.json_encoder.cache_warmer.lazy_ghost', LazyGhostCacheWarmer::class) ->args([ - tagged_iterator('json_encoder.encodable'), + abstract_arg('encodable class names'), param('.json_encoder.lazy_ghosts_dir'), ]) ->tag('kernel.cache_warmer') diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 9cb89207d..ba26d85d1 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -1006,11 +1006,7 @@ - - - - - + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index b4b8eb875..13f3d8e1d 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -973,7 +973,6 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor ], 'json_encoder' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(JsonEncoder::class), - 'paths' => [], ], ]; } diff --git a/Tests/Functional/app/JsonEncoder/config.yml b/Tests/Functional/app/JsonEncoder/config.yml index 55fdf53f5..a92aa3969 100644 --- a/Tests/Functional/app/JsonEncoder/config.yml +++ b/Tests/Functional/app/JsonEncoder/config.yml @@ -4,10 +4,7 @@ imports: framework: http_method_override: false type_info: ~ - json_encoder: - enabled: true - paths: - Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\: '../../Tests/Functional/app/JsonEncoder/Dto/*' + json_encoder: ~ services: _defaults: From 1dea3d3896ab9cad87c2f2048ca6356a8a2f49c2 Mon Sep 17 00:00:00 2001 From: Florian Merle Date: Mon, 16 Dec 2024 15:20:00 +0100 Subject: [PATCH 016/111] [FrameworkBundle] Always display service arguments & deprecate `--show-arguments` option for `debug:container` --- CHANGELOG.md | 1 + Command/ContainerDebugCommand.php | 5 +- Console/Descriptor/JsonDescriptor.php | 25 ++++--- Console/Descriptor/MarkdownDescriptor.php | 7 +- Console/Descriptor/TextDescriptor.php | 3 +- Console/Descriptor/XmlDescriptor.php | 30 ++++----- .../Descriptor/AbstractDescriptorTestCase.php | 4 +- .../Descriptor/alias_with_definition_1.json | 65 +++++++++++++++++++ .../Descriptor/alias_with_definition_1.md | 1 + .../Descriptor/alias_with_definition_1.txt | 42 +++++++----- .../Descriptor/alias_with_definition_1.xml | 20 ++++++ .../Descriptor/alias_with_definition_2.json | 1 + .../Descriptor/alias_with_definition_2.md | 1 + .../Fixtures/Descriptor/builder_1_public.json | 63 ++++++++++++++++++ Tests/Fixtures/Descriptor/builder_1_public.md | 3 + .../Fixtures/Descriptor/builder_1_public.xml | 20 ++++++ .../Descriptor/builder_1_services.json | 2 + .../Fixtures/Descriptor/builder_1_services.md | 2 + Tests/Fixtures/Descriptor/builder_1_tag1.json | 1 + Tests/Fixtures/Descriptor/builder_1_tag1.md | 1 + Tests/Fixtures/Descriptor/builder_1_tags.json | 3 + Tests/Fixtures/Descriptor/builder_1_tags.md | 3 + .../Descriptor/builder_priority_tag.json | 4 ++ .../Descriptor/builder_priority_tag.md | 4 ++ Tests/Fixtures/Descriptor/definition_1.json | 61 +++++++++++++++++ Tests/Fixtures/Descriptor/definition_1.md | 1 + Tests/Fixtures/Descriptor/definition_1.txt | 43 +++++++----- Tests/Fixtures/Descriptor/definition_1.xml | 20 ++++++ Tests/Fixtures/Descriptor/definition_2.json | 1 + Tests/Fixtures/Descriptor/definition_2.md | 1 + Tests/Fixtures/Descriptor/definition_3.json | 1 + Tests/Fixtures/Descriptor/definition_3.md | 1 + .../Descriptor/definition_without_class.json | 1 + .../Descriptor/definition_without_class.md | 1 + .../Descriptor/existing_class_def_1.json | 1 + .../Descriptor/existing_class_def_1.md | 1 + .../Descriptor/existing_class_def_2.json | 1 + .../Descriptor/existing_class_def_2.md | 1 + .../Functional/ContainerDebugCommandTest.php | 18 +++++ 39 files changed, 389 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02bfe6af4..97fa33a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` * Add JsonEncoder services and configuration * Add new `framework.property_info.with_constructor_extractor` option to allow enabling or disabling the constructor extractor integration + * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown 7.2 --- diff --git a/Command/ContainerDebugCommand.php b/Command/ContainerDebugCommand.php index 46cdca9ab..ca3aacf73 100644 --- a/Command/ContainerDebugCommand.php +++ b/Command/ContainerDebugCommand.php @@ -151,6 +151,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $tag = $this->findProperTagName($input, $errorIo, $object, $tag); $options = ['tag' => $tag]; } elseif ($name = $input->getArgument('name')) { + if ($input->getOption('show-arguments')) { + $errorIo->warning('The "--show-arguments" option is deprecated.'); + } + $name = $this->findProperServiceName($input, $errorIo, $object, $name, $input->getOption('show-hidden')); $options = ['id' => $name]; } elseif ($input->getOption('deprecations')) { @@ -161,7 +165,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $helper = new DescriptorHelper(); $options['format'] = $input->getOption('format'); - $options['show_arguments'] = $input->getOption('show-arguments'); $options['show_hidden'] = $input->getOption('show-hidden'); $options['raw_text'] = $input->getOption('raw'); $options['output'] = $io; diff --git a/Console/Descriptor/JsonDescriptor.php b/Console/Descriptor/JsonDescriptor.php index 5b83f0746..c7705a1a0 100644 --- a/Console/Descriptor/JsonDescriptor.php +++ b/Console/Descriptor/JsonDescriptor.php @@ -63,7 +63,7 @@ protected function describeContainerTags(ContainerBuilder $container, array $opt foreach ($this->findDefinitionsByTag($container, $showHidden) as $tag => $definitions) { $data[$tag] = []; foreach ($definitions as $definition) { - $data[$tag][] = $this->getContainerDefinitionData($definition, true, false, $container, $options['id'] ?? null); + $data[$tag][] = $this->getContainerDefinitionData($definition, true, $container, $options['id'] ?? null); } } @@ -79,7 +79,7 @@ protected function describeContainerService(object $service, array $options = [] if ($service instanceof Alias) { $this->describeContainerAlias($service, $options, $container); } elseif ($service instanceof Definition) { - $this->writeData($this->getContainerDefinitionData($service, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'], $container, $options['id']), $options); + $this->writeData($this->getContainerDefinitionData($service, isset($options['omit_tags']) && $options['omit_tags'], $container, $options['id']), $options); } else { $this->writeData($service::class, $options); } @@ -92,7 +92,6 @@ protected function describeContainerServices(ContainerBuilder $container, array : $this->sortServiceIds($container->getServiceIds()); $showHidden = isset($options['show_hidden']) && $options['show_hidden']; $omitTags = isset($options['omit_tags']) && $options['omit_tags']; - $showArguments = isset($options['show_arguments']) && $options['show_arguments']; $data = ['definitions' => [], 'aliases' => [], 'services' => []]; if (isset($options['filter'])) { @@ -112,7 +111,7 @@ protected function describeContainerServices(ContainerBuilder $container, array if ($service->hasTag('container.excluded')) { continue; } - $data['definitions'][$serviceId] = $this->getContainerDefinitionData($service, $omitTags, $showArguments, $container, $serviceId); + $data['definitions'][$serviceId] = $this->getContainerDefinitionData($service, $omitTags, $container, $serviceId); } else { $data['services'][$serviceId] = $service::class; } @@ -123,7 +122,7 @@ protected function describeContainerServices(ContainerBuilder $container, array protected function describeContainerDefinition(Definition $definition, array $options = [], ?ContainerBuilder $container = null): void { - $this->writeData($this->getContainerDefinitionData($definition, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'], $container, $options['id'] ?? null), $options); + $this->writeData($this->getContainerDefinitionData($definition, isset($options['omit_tags']) && $options['omit_tags'], $container, $options['id'] ?? null), $options); } protected function describeContainerAlias(Alias $alias, array $options = [], ?ContainerBuilder $container = null): void @@ -135,7 +134,7 @@ protected function describeContainerAlias(Alias $alias, array $options = [], ?Co } $this->writeData( - [$this->getContainerAliasData($alias), $this->getContainerDefinitionData($container->getDefinition((string) $alias), isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'], $container, (string) $alias)], + [$this->getContainerAliasData($alias), $this->getContainerDefinitionData($container->getDefinition((string) $alias), isset($options['omit_tags']) && $options['omit_tags'], $container, (string) $alias)], array_merge($options, ['id' => (string) $alias]) ); } @@ -245,7 +244,7 @@ protected function sortParameters(ParameterBag $parameters): array return $sortedParameters; } - private function getContainerDefinitionData(Definition $definition, bool $omitTags = false, bool $showArguments = false, ?ContainerBuilder $container = null, ?string $id = null): array + private function getContainerDefinitionData(Definition $definition, bool $omitTags = false, ?ContainerBuilder $container = null, ?string $id = null): array { $data = [ 'class' => (string) $definition->getClass(), @@ -269,9 +268,7 @@ private function getContainerDefinitionData(Definition $definition, bool $omitTa $data['description'] = $classDescription; } - if ($showArguments) { - $data['arguments'] = $this->describeValue($definition->getArguments(), $omitTags, $showArguments, $container, $id); - } + $data['arguments'] = $this->describeValue($definition->getArguments(), $omitTags, $container, $id); $data['file'] = $definition->getFile(); @@ -418,12 +415,12 @@ private function getCallableData(mixed $callable): array throw new \InvalidArgumentException('Callable is not describable.'); } - private function describeValue($value, bool $omitTags, bool $showArguments, ?ContainerBuilder $container = null, ?string $id = null): mixed + private function describeValue($value, bool $omitTags, ?ContainerBuilder $container = null, ?string $id = null): mixed { if (\is_array($value)) { $data = []; foreach ($value as $k => $v) { - $data[$k] = $this->describeValue($v, $omitTags, $showArguments, $container, $id); + $data[$k] = $this->describeValue($v, $omitTags, $container, $id); } return $data; @@ -445,11 +442,11 @@ private function describeValue($value, bool $omitTags, bool $showArguments, ?Con } if ($value instanceof ArgumentInterface) { - return $this->describeValue($value->getValues(), $omitTags, $showArguments, $container, $id); + return $this->describeValue($value->getValues(), $omitTags, $container, $id); } if ($value instanceof Definition) { - return $this->getContainerDefinitionData($value, $omitTags, $showArguments, $container, $id); + return $this->getContainerDefinitionData($value, $omitTags, $container, $id); } return $value; diff --git a/Console/Descriptor/MarkdownDescriptor.php b/Console/Descriptor/MarkdownDescriptor.php index 5203d14c3..d057c598d 100644 --- a/Console/Descriptor/MarkdownDescriptor.php +++ b/Console/Descriptor/MarkdownDescriptor.php @@ -155,7 +155,6 @@ protected function describeContainerServices(ContainerBuilder $container, array $serviceIds = isset($options['tag']) && $options['tag'] ? $this->sortTaggedServicesByPriority($container->findTaggedServiceIds($options['tag'])) : $this->sortServiceIds($container->getServiceIds()); - $showArguments = isset($options['show_arguments']) && $options['show_arguments']; $services = ['definitions' => [], 'aliases' => [], 'services' => []]; if (isset($options['filter'])) { @@ -185,7 +184,7 @@ protected function describeContainerServices(ContainerBuilder $container, array $this->write("\n\nDefinitions\n-----------\n"); foreach ($services['definitions'] as $id => $service) { $this->write("\n"); - $this->describeContainerDefinition($service, ['id' => $id, 'show_arguments' => $showArguments], $container); + $this->describeContainerDefinition($service, ['id' => $id], $container); } } @@ -231,9 +230,7 @@ protected function describeContainerDefinition(Definition $definition, array $op $output .= "\n".'- Deprecated: no'; } - if (isset($options['show_arguments']) && $options['show_arguments']) { - $output .= "\n".'- Arguments: '.($definition->getArguments() ? 'yes' : 'no'); - } + $output .= "\n".'- Arguments: '.($definition->getArguments() ? 'yes' : 'no'); if ($definition->getFile()) { $output .= "\n".'- File: `'.$definition->getFile().'`'; diff --git a/Console/Descriptor/TextDescriptor.php b/Console/Descriptor/TextDescriptor.php index 5efaab496..12b345411 100644 --- a/Console/Descriptor/TextDescriptor.php +++ b/Console/Descriptor/TextDescriptor.php @@ -351,9 +351,8 @@ protected function describeContainerDefinition(Definition $definition, array $op } } - $showArguments = isset($options['show_arguments']) && $options['show_arguments']; $argumentsInformation = []; - if ($showArguments && ($arguments = $definition->getArguments())) { + if ($arguments = $definition->getArguments()) { foreach ($arguments as $argument) { if ($argument instanceof ServiceClosureArgument) { $argument = $argument->getValues()[0]; diff --git a/Console/Descriptor/XmlDescriptor.php b/Console/Descriptor/XmlDescriptor.php index c41ac296f..8daa61d2a 100644 --- a/Console/Descriptor/XmlDescriptor.php +++ b/Console/Descriptor/XmlDescriptor.php @@ -59,17 +59,17 @@ protected function describeContainerService(object $service, array $options = [] throw new \InvalidArgumentException('An "id" option must be provided.'); } - $this->writeDocument($this->getContainerServiceDocument($service, $options['id'], $container, isset($options['show_arguments']) && $options['show_arguments'])); + $this->writeDocument($this->getContainerServiceDocument($service, $options['id'], $container)); } protected function describeContainerServices(ContainerBuilder $container, array $options = []): void { - $this->writeDocument($this->getContainerServicesDocument($container, $options['tag'] ?? null, isset($options['show_hidden']) && $options['show_hidden'], isset($options['show_arguments']) && $options['show_arguments'], $options['filter'] ?? null)); + $this->writeDocument($this->getContainerServicesDocument($container, $options['tag'] ?? null, isset($options['show_hidden']) && $options['show_hidden'], $options['filter'] ?? null)); } protected function describeContainerDefinition(Definition $definition, array $options = [], ?ContainerBuilder $container = null): void { - $this->writeDocument($this->getContainerDefinitionDocument($definition, $options['id'] ?? null, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'], $container)); + $this->writeDocument($this->getContainerDefinitionDocument($definition, $options['id'] ?? null, isset($options['omit_tags']) && $options['omit_tags'], $container)); } protected function describeContainerAlias(Alias $alias, array $options = [], ?ContainerBuilder $container = null): void @@ -83,7 +83,7 @@ protected function describeContainerAlias(Alias $alias, array $options = [], ?Co return; } - $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($container->getDefinition((string) $alias), (string) $alias, false, false, $container)->childNodes->item(0), true)); + $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($container->getDefinition((string) $alias), (string) $alias, false, $container)->childNodes->item(0), true)); $this->writeDocument($dom); } @@ -260,7 +260,7 @@ private function getContainerTagsDocument(ContainerBuilder $container, bool $sho $tagXML->setAttribute('name', $tag); foreach ($definitions as $serviceId => $definition) { - $definitionXML = $this->getContainerDefinitionDocument($definition, $serviceId, true, false, $container); + $definitionXML = $this->getContainerDefinitionDocument($definition, $serviceId, true, $container); $tagXML->appendChild($dom->importNode($definitionXML->childNodes->item(0), true)); } } @@ -268,17 +268,17 @@ private function getContainerTagsDocument(ContainerBuilder $container, bool $sho return $dom; } - private function getContainerServiceDocument(object $service, string $id, ?ContainerBuilder $container = null, bool $showArguments = false): \DOMDocument + private function getContainerServiceDocument(object $service, string $id, ?ContainerBuilder $container = null): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); if ($service instanceof Alias) { $dom->appendChild($dom->importNode($this->getContainerAliasDocument($service, $id)->childNodes->item(0), true)); if ($container) { - $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($container->getDefinition((string) $service), (string) $service, false, $showArguments, $container)->childNodes->item(0), true)); + $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($container->getDefinition((string) $service), (string) $service, false, $container)->childNodes->item(0), true)); } } elseif ($service instanceof Definition) { - $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($service, $id, false, $showArguments, $container)->childNodes->item(0), true)); + $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($service, $id, false, $container)->childNodes->item(0), true)); } else { $dom->appendChild($serviceXML = $dom->createElement('service')); $serviceXML->setAttribute('id', $id); @@ -288,7 +288,7 @@ private function getContainerServiceDocument(object $service, string $id, ?Conta return $dom; } - private function getContainerServicesDocument(ContainerBuilder $container, ?string $tag = null, bool $showHidden = false, bool $showArguments = false, ?callable $filter = null): \DOMDocument + private function getContainerServicesDocument(ContainerBuilder $container, ?string $tag = null, bool $showHidden = false, ?callable $filter = null): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($containerXML = $dom->createElement('container')); @@ -311,14 +311,14 @@ private function getContainerServicesDocument(ContainerBuilder $container, ?stri continue; } - $serviceXML = $this->getContainerServiceDocument($service, $serviceId, null, $showArguments); + $serviceXML = $this->getContainerServiceDocument($service, $serviceId, null); $containerXML->appendChild($containerXML->ownerDocument->importNode($serviceXML->childNodes->item(0), true)); } return $dom; } - private function getContainerDefinitionDocument(Definition $definition, ?string $id = null, bool $omitTags = false, bool $showArguments = false, ?ContainerBuilder $container = null): \DOMDocument + private function getContainerDefinitionDocument(Definition $definition, ?string $id = null, bool $omitTags = false, ?ContainerBuilder $container = null): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($serviceXML = $dom->createElement('definition')); @@ -378,10 +378,8 @@ private function getContainerDefinitionDocument(Definition $definition, ?string } } - if ($showArguments) { - foreach ($this->getArgumentNodes($definition->getArguments(), $dom, $container) as $node) { - $serviceXML->appendChild($node); - } + foreach ($this->getArgumentNodes($definition->getArguments(), $dom, $container) as $node) { + $serviceXML->appendChild($node); } if (!$omitTags) { @@ -443,7 +441,7 @@ private function getArgumentNodes(array $arguments, \DOMDocument $dom, ?Containe $argumentXML->appendChild($childArgumentXML); } } elseif ($argument instanceof Definition) { - $argumentXML->appendChild($dom->importNode($this->getContainerDefinitionDocument($argument, null, false, true, $container)->childNodes->item(0), true)); + $argumentXML->appendChild($dom->importNode($this->getContainerDefinitionDocument($argument, null, false, $container)->childNodes->item(0), true)); } elseif ($argument instanceof AbstractArgument) { $argumentXML->setAttribute('type', 'abstract'); $argumentXML->appendChild(new \DOMText($argument->getText())); diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index dde1f000b..477bd1014 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -110,7 +110,7 @@ public static function getDescribeContainerDefinitionTestData(): array /** @dataProvider getDescribeContainerDefinitionWithArgumentsShownTestData */ public function testDescribeContainerDefinitionWithArgumentsShown(Definition $definition, $expectedDescription) { - $this->assertDescription($expectedDescription, $definition, ['show_arguments' => true]); + $this->assertDescription($expectedDescription, $definition, []); } public static function getDescribeContainerDefinitionWithArgumentsShownTestData(): array @@ -307,7 +307,7 @@ private static function getContainerBuilderDescriptionTestData(array $objects): 'public' => ['show_hidden' => false], 'tag1' => ['show_hidden' => true, 'tag' => 'tag1'], 'tags' => ['group_by' => 'tags', 'show_hidden' => true], - 'arguments' => ['show_hidden' => false, 'show_arguments' => true], + 'arguments' => ['show_hidden' => false], ]; $data = []; diff --git a/Tests/Fixtures/Descriptor/alias_with_definition_1.json b/Tests/Fixtures/Descriptor/alias_with_definition_1.json index a2c015faa..e4acc2a83 100644 --- a/Tests/Fixtures/Descriptor/alias_with_definition_1.json +++ b/Tests/Fixtures/Descriptor/alias_with_definition_1.json @@ -13,6 +13,71 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [ + { + "type": "service", + "id": ".definition_2" + }, + "%parameter%", + { + "class": "inline_service", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "deprecated": false, + "arguments": [ + "arg1", + "arg2" + ], + "file": null, + "tags": [], + "usages": [ + "alias_1" + ] + }, + [ + "foo", + { + "type": "service", + "id": ".definition_2" + }, + { + "class": "inline_service", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "deprecated": false, + "arguments": [], + "file": null, + "tags": [], + "usages": [ + "alias_1" + ] + } + ], + [ + { + "type": "service", + "id": "definition_1" + }, + { + "type": "service", + "id": ".definition_2" + } + ], + { + "type": "abstract", + "text": "placeholder" + } + ], "file": null, "factory_class": "Full\\Qualified\\FactoryClass", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/alias_with_definition_1.md b/Tests/Fixtures/Descriptor/alias_with_definition_1.md index c92c8435f..fd94e43e9 100644 --- a/Tests/Fixtures/Descriptor/alias_with_definition_1.md +++ b/Tests/Fixtures/Descriptor/alias_with_definition_1.md @@ -14,6 +14,7 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: yes - Factory Class: `Full\Qualified\FactoryClass` - Factory Method: `get` - Usages: alias_1 diff --git a/Tests/Fixtures/Descriptor/alias_with_definition_1.txt b/Tests/Fixtures/Descriptor/alias_with_definition_1.txt index 7883d51c0..eea6c70b1 100644 --- a/Tests/Fixtures/Descriptor/alias_with_definition_1.txt +++ b/Tests/Fixtures/Descriptor/alias_with_definition_1.txt @@ -3,20 +3,28 @@ Information for Service "service_1" =================================== - ---------------- ----------------------------- -  Option   Value  - ---------------- ----------------------------- - Service ID service_1 - Class Full\Qualified\Class1 - Tags - - Public yes - Synthetic no - Lazy yes - Shared yes - Abstract yes - Autowired no - Autoconfigured no - Factory Class Full\Qualified\FactoryClass - Factory Method get - Usages alias_1 - ---------------- ----------------------------- + ---------------- --------------------------------- +  Option   Value  + ---------------- --------------------------------- + Service ID service_1 + Class Full\Qualified\Class1 + Tags - + Public yes + Synthetic no + Lazy yes + Shared yes + Abstract yes + Autowired no + Autoconfigured no + Factory Class Full\Qualified\FactoryClass + Factory Method get + Arguments Service(.definition_2) + %parameter% + Inlined Service + Array (3 element(s)) + Iterator (2 element(s)) + - Service(definition_1) + - Service(.definition_2) + Abstract argument (placeholder) + Usages alias_1 + ---------------- --------------------------------- diff --git a/Tests/Fixtures/Descriptor/alias_with_definition_1.xml b/Tests/Fixtures/Descriptor/alias_with_definition_1.xml index 06c8406da..3eab915ab 100644 --- a/Tests/Fixtures/Descriptor/alias_with_definition_1.xml +++ b/Tests/Fixtures/Descriptor/alias_with_definition_1.xml @@ -2,6 +2,26 @@ + + %parameter% + + + arg1 + arg2 + + + + foo + + + + + + + + + + placeholder alias_1 diff --git a/Tests/Fixtures/Descriptor/alias_with_definition_2.json b/Tests/Fixtures/Descriptor/alias_with_definition_2.json index f3b930983..e59ff8524 100644 --- a/Tests/Fixtures/Descriptor/alias_with_definition_2.json +++ b/Tests/Fixtures/Descriptor/alias_with_definition_2.json @@ -13,6 +13,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/alias_with_definition_2.md b/Tests/Fixtures/Descriptor/alias_with_definition_2.md index 3ec9516a3..045da01b0 100644 --- a/Tests/Fixtures/Descriptor/alias_with_definition_2.md +++ b/Tests/Fixtures/Descriptor/alias_with_definition_2.md @@ -14,6 +14,7 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` diff --git a/Tests/Fixtures/Descriptor/builder_1_public.json b/Tests/Fixtures/Descriptor/builder_1_public.json index 0d6198b07..28d64c611 100644 --- a/Tests/Fixtures/Descriptor/builder_1_public.json +++ b/Tests/Fixtures/Descriptor/builder_1_public.json @@ -10,6 +10,67 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [ + { + "type": "service", + "id": ".definition_2" + }, + "%parameter%", + { + "class": "inline_service", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "deprecated": false, + "arguments": [ + "arg1", + "arg2" + ], + "file": null, + "tags": [], + "usages": [] + }, + [ + "foo", + { + "type": "service", + "id": ".definition_2" + }, + { + "class": "inline_service", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "deprecated": false, + "arguments": [], + "file": null, + "tags": [], + "usages": [] + } + ], + [ + { + "type": "service", + "id": "definition_1" + }, + { + "type": "service", + "id": ".definition_2" + } + ], + { + "type": "abstract", + "text": "placeholder" + } + ], "file": null, "factory_class": "Full\\Qualified\\FactoryClass", "factory_method": "get", @@ -26,6 +87,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": null, "tags": [], "usages": [] @@ -41,6 +103,7 @@ "autoconfigure": false, "deprecated": false, "description": "ContainerInterface is the interface implemented by service container classes.", + "arguments": [], "file": null, "tags": [], "usages": [] diff --git a/Tests/Fixtures/Descriptor/builder_1_public.md b/Tests/Fixtures/Descriptor/builder_1_public.md index 2532a2c4e..57a209ecb 100644 --- a/Tests/Fixtures/Descriptor/builder_1_public.md +++ b/Tests/Fixtures/Descriptor/builder_1_public.md @@ -15,6 +15,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: yes - Factory Class: `Full\Qualified\FactoryClass` - Factory Method: `get` - Usages: none @@ -30,6 +31,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - Usages: none ### service_container @@ -44,6 +46,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - Usages: none diff --git a/Tests/Fixtures/Descriptor/builder_1_public.xml b/Tests/Fixtures/Descriptor/builder_1_public.xml index 3b13b7264..fdddad653 100644 --- a/Tests/Fixtures/Descriptor/builder_1_public.xml +++ b/Tests/Fixtures/Descriptor/builder_1_public.xml @@ -3,6 +3,26 @@ + + %parameter% + + + arg1 + arg2 + + + + foo + + + + + + + + + + placeholder diff --git a/Tests/Fixtures/Descriptor/builder_1_services.json b/Tests/Fixtures/Descriptor/builder_1_services.json index ac6d122ce..473709247 100644 --- a/Tests/Fixtures/Descriptor/builder_1_services.json +++ b/Tests/Fixtures/Descriptor/builder_1_services.json @@ -10,6 +10,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", @@ -65,6 +66,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "inline factory service (Full\\Qualified\\FactoryClass)", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/builder_1_services.md b/Tests/Fixtures/Descriptor/builder_1_services.md index 6dfab327d..64801e03b 100644 --- a/Tests/Fixtures/Descriptor/builder_1_services.md +++ b/Tests/Fixtures/Descriptor/builder_1_services.md @@ -15,6 +15,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` @@ -40,6 +41,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: inline factory service (`Full\Qualified\FactoryClass`) - Factory Method: `get` diff --git a/Tests/Fixtures/Descriptor/builder_1_tag1.json b/Tests/Fixtures/Descriptor/builder_1_tag1.json index 5e60f26d1..cead51aa9 100644 --- a/Tests/Fixtures/Descriptor/builder_1_tag1.json +++ b/Tests/Fixtures/Descriptor/builder_1_tag1.json @@ -10,6 +10,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/builder_1_tag1.md b/Tests/Fixtures/Descriptor/builder_1_tag1.md index aeae0d9f2..8e9229349 100644 --- a/Tests/Fixtures/Descriptor/builder_1_tag1.md +++ b/Tests/Fixtures/Descriptor/builder_1_tag1.md @@ -15,6 +15,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` diff --git a/Tests/Fixtures/Descriptor/builder_1_tags.json b/Tests/Fixtures/Descriptor/builder_1_tags.json index 518f694ea..6775a0e36 100644 --- a/Tests/Fixtures/Descriptor/builder_1_tags.json +++ b/Tests/Fixtures/Descriptor/builder_1_tags.json @@ -10,6 +10,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", @@ -30,6 +31,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", @@ -50,6 +52,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/builder_1_tags.md b/Tests/Fixtures/Descriptor/builder_1_tags.md index 80da2ddaf..cc0496e28 100644 --- a/Tests/Fixtures/Descriptor/builder_1_tags.md +++ b/Tests/Fixtures/Descriptor/builder_1_tags.md @@ -15,6 +15,7 @@ tag1 - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` @@ -36,6 +37,7 @@ tag2 - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` @@ -57,6 +59,7 @@ tag3 - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` diff --git a/Tests/Fixtures/Descriptor/builder_priority_tag.json b/Tests/Fixtures/Descriptor/builder_priority_tag.json index 75d893297..d9c3d050c 100644 --- a/Tests/Fixtures/Descriptor/builder_priority_tag.json +++ b/Tests/Fixtures/Descriptor/builder_priority_tag.json @@ -10,6 +10,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "tags": [ { @@ -40,6 +41,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", @@ -77,6 +79,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "tags": [ { @@ -98,6 +101,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "tags": [ { diff --git a/Tests/Fixtures/Descriptor/builder_priority_tag.md b/Tests/Fixtures/Descriptor/builder_priority_tag.md index 7137e1b1d..90ef56ee4 100644 --- a/Tests/Fixtures/Descriptor/builder_priority_tag.md +++ b/Tests/Fixtures/Descriptor/builder_priority_tag.md @@ -15,6 +15,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Tag: `tag1` - Attr3: val3 @@ -36,6 +37,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` @@ -59,6 +61,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Tag: `tag1` - Priority: 0 @@ -75,6 +78,7 @@ Definitions - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Tag: `tag1` - Attr1: val1 diff --git a/Tests/Fixtures/Descriptor/definition_1.json b/Tests/Fixtures/Descriptor/definition_1.json index 735b3df47..b0a612030 100644 --- a/Tests/Fixtures/Descriptor/definition_1.json +++ b/Tests/Fixtures/Descriptor/definition_1.json @@ -8,6 +8,67 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [ + { + "type": "service", + "id": ".definition_2" + }, + "%parameter%", + { + "class": "inline_service", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "deprecated": false, + "arguments": [ + "arg1", + "arg2" + ], + "file": null, + "tags": [], + "usages": [] + }, + [ + "foo", + { + "type": "service", + "id": ".definition_2" + }, + { + "class": "inline_service", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "deprecated": false, + "arguments": [], + "file": null, + "tags": [], + "usages": [] + } + ], + [ + { + "type": "service", + "id": "definition_1" + }, + { + "type": "service", + "id": ".definition_2" + } + ], + { + "type": "abstract", + "text": "placeholder" + } + ], "file": null, "factory_class": "Full\\Qualified\\FactoryClass", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/definition_1.md b/Tests/Fixtures/Descriptor/definition_1.md index c7ad62954..b99162bbf 100644 --- a/Tests/Fixtures/Descriptor/definition_1.md +++ b/Tests/Fixtures/Descriptor/definition_1.md @@ -7,6 +7,7 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: yes - Factory Class: `Full\Qualified\FactoryClass` - Factory Method: `get` - Usages: none diff --git a/Tests/Fixtures/Descriptor/definition_1.txt b/Tests/Fixtures/Descriptor/definition_1.txt index 8ec7be868..775a04c84 100644 --- a/Tests/Fixtures/Descriptor/definition_1.txt +++ b/Tests/Fixtures/Descriptor/definition_1.txt @@ -1,18 +1,25 @@ - ---------------- ----------------------------- -  Option   Value  - ---------------- ----------------------------- - Service ID - - Class Full\Qualified\Class1 - Tags - - Public yes - Synthetic no - Lazy yes - Shared yes - Abstract yes - Autowired no - Autoconfigured no - Factory Class Full\Qualified\FactoryClass - Factory Method get - Usages none - ---------------- ----------------------------- - + ---------------- --------------------------------- +  Option   Value  + ---------------- --------------------------------- + Service ID - + Class Full\Qualified\Class1 + Tags - + Public yes + Synthetic no + Lazy yes + Shared yes + Abstract yes + Autowired no + Autoconfigured no + Factory Class Full\Qualified\FactoryClass + Factory Method get + Arguments Service(.definition_2) + %parameter% + Inlined Service + Array (3 element(s)) + Iterator (2 element(s)) + - Service(definition_1) + - Service(.definition_2) + Abstract argument (placeholder) + Usages none + ---------------- --------------------------------- diff --git a/Tests/Fixtures/Descriptor/definition_1.xml b/Tests/Fixtures/Descriptor/definition_1.xml index be2b16b57..eba7e7bbd 100644 --- a/Tests/Fixtures/Descriptor/definition_1.xml +++ b/Tests/Fixtures/Descriptor/definition_1.xml @@ -1,4 +1,24 @@ + + %parameter% + + + arg1 + arg2 + + + + foo + + + + + + + + + + placeholder diff --git a/Tests/Fixtures/Descriptor/definition_2.json b/Tests/Fixtures/Descriptor/definition_2.json index a661428c9..eeeb6f44a 100644 --- a/Tests/Fixtures/Descriptor/definition_2.json +++ b/Tests/Fixtures/Descriptor/definition_2.json @@ -8,6 +8,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "factory.service", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/definition_2.md b/Tests/Fixtures/Descriptor/definition_2.md index 486f35fb7..5b427bff5 100644 --- a/Tests/Fixtures/Descriptor/definition_2.md +++ b/Tests/Fixtures/Descriptor/definition_2.md @@ -7,6 +7,7 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: `factory.service` - Factory Method: `get` diff --git a/Tests/Fixtures/Descriptor/definition_3.json b/Tests/Fixtures/Descriptor/definition_3.json index 11768d0de..c96c06d63 100644 --- a/Tests/Fixtures/Descriptor/definition_3.json +++ b/Tests/Fixtures/Descriptor/definition_3.json @@ -8,6 +8,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": "\/path\/to\/file", "factory_service": "inline factory service (Full\\Qualified\\FactoryClass)", "factory_method": "get", diff --git a/Tests/Fixtures/Descriptor/definition_3.md b/Tests/Fixtures/Descriptor/definition_3.md index 8a9651641..5bfafe3d0 100644 --- a/Tests/Fixtures/Descriptor/definition_3.md +++ b/Tests/Fixtures/Descriptor/definition_3.md @@ -7,6 +7,7 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - File: `/path/to/file` - Factory Service: inline factory service (`Full\Qualified\FactoryClass`) - Factory Method: `get` diff --git a/Tests/Fixtures/Descriptor/definition_without_class.json b/Tests/Fixtures/Descriptor/definition_without_class.json index 078f7cdca..c1305ac0c 100644 --- a/Tests/Fixtures/Descriptor/definition_without_class.json +++ b/Tests/Fixtures/Descriptor/definition_without_class.json @@ -8,6 +8,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": null, "tags": [], "usages": [] diff --git a/Tests/Fixtures/Descriptor/definition_without_class.md b/Tests/Fixtures/Descriptor/definition_without_class.md index be221535f..7c7bad74d 100644 --- a/Tests/Fixtures/Descriptor/definition_without_class.md +++ b/Tests/Fixtures/Descriptor/definition_without_class.md @@ -7,4 +7,5 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - Usages: none diff --git a/Tests/Fixtures/Descriptor/existing_class_def_1.json b/Tests/Fixtures/Descriptor/existing_class_def_1.json index c6de89ce5..00c8a5be0 100644 --- a/Tests/Fixtures/Descriptor/existing_class_def_1.json +++ b/Tests/Fixtures/Descriptor/existing_class_def_1.json @@ -9,6 +9,7 @@ "autoconfigure": false, "deprecated": false, "description": "This is a class with a doc comment.", + "arguments": [], "file": null, "tags": [], "usages": [] diff --git a/Tests/Fixtures/Descriptor/existing_class_def_1.md b/Tests/Fixtures/Descriptor/existing_class_def_1.md index 132147324..907f69460 100644 --- a/Tests/Fixtures/Descriptor/existing_class_def_1.md +++ b/Tests/Fixtures/Descriptor/existing_class_def_1.md @@ -8,4 +8,5 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - Usages: none diff --git a/Tests/Fixtures/Descriptor/existing_class_def_2.json b/Tests/Fixtures/Descriptor/existing_class_def_2.json index 7b387fd86..88a59851a 100644 --- a/Tests/Fixtures/Descriptor/existing_class_def_2.json +++ b/Tests/Fixtures/Descriptor/existing_class_def_2.json @@ -8,6 +8,7 @@ "autowire": false, "autoconfigure": false, "deprecated": false, + "arguments": [], "file": null, "tags": [], "usages": [] diff --git a/Tests/Fixtures/Descriptor/existing_class_def_2.md b/Tests/Fixtures/Descriptor/existing_class_def_2.md index 0526ba117..8fd89fb0f 100644 --- a/Tests/Fixtures/Descriptor/existing_class_def_2.md +++ b/Tests/Fixtures/Descriptor/existing_class_def_2.md @@ -7,4 +7,5 @@ - Autowired: no - Autoconfigured: no - Deprecated: no +- Arguments: no - Usages: none diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index bb80a4484..b5d395a23 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -341,4 +341,22 @@ public static function provideCompletionSuggestions(): iterable ['txt', 'xml', 'json', 'md'], ]; } + + public function testShowArgumentsNotProvidedShouldTriggerDeprecation() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); + $path = sprintf('%s/%sDeprecations.log', static::$kernel->getContainer()->getParameter('kernel.build_dir'), static::$kernel->getContainer()->getParameter('kernel.container_class')); + @unlink($path); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + @unlink(static::getContainer()->getParameter('debug.container.dump')); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:container', 'name' => 'router', '--show-arguments' => true]); + + $tester->assertCommandIsSuccessful(); + $this->assertStringContainsString('[WARNING] The "--show-arguments" option is deprecated.', $tester->getDisplay()); + } } From 27fee457545372bb298d36ce6d1d84079bf1751a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 7 Jan 2025 14:24:39 +0100 Subject: [PATCH 017/111] add compiler pass only if it exists --- FrameworkBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FrameworkBundle.php b/FrameworkBundle.php index c16f9b9c1..89d9744f5 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -189,7 +189,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new VirtualRequestStackPass()); $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING); - $container->addCompilerPass(new EncodablePass()); + $this->addCompilerPassIfExists($container, EncodablePass::class); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); From 9e4794e73f2a2eb8230ff08bc3bc6926921945dd Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 7 Jan 2025 15:57:51 +0100 Subject: [PATCH 018/111] [PropertyAccess] Move dependency definitions outside of Extension --- DependencyInjection/FrameworkExtension.php | 4 ---- Resources/config/property_access.php | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 79c876161..5a86acb54 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -143,9 +143,7 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; -use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; -use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; use Symfony\Component\RateLimiter\LimiterInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\RateLimiter\Storage\CacheStorage; @@ -1810,8 +1808,6 @@ private function registerPropertyAccessConfiguration(array $config, ContainerBui ->getDefinition('property_accessor') ->replaceArgument(0, $magicMethods) ->replaceArgument(1, $throw) - ->replaceArgument(3, new Reference(PropertyReadInfoExtractorInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) - ->replaceArgument(4, new Reference(PropertyWriteInfoExtractorInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) ; } diff --git a/Resources/config/property_access.php b/Resources/config/property_access.php index 85ab9f18e..4c9feb660 100644 --- a/Resources/config/property_access.php +++ b/Resources/config/property_access.php @@ -13,6 +13,8 @@ use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; return static function (ContainerConfigurator $container) { $container->services() @@ -21,8 +23,8 @@ abstract_arg('magic methods allowed, set by the extension'), abstract_arg('throw exceptions, set by the extension'), service('cache.property_access')->ignoreOnInvalid(), - abstract_arg('propertyReadInfoExtractor, set by the extension'), - abstract_arg('propertyWriteInfoExtractor, set by the extension'), + service(PropertyReadInfoExtractorInterface::class)->nullOnInvalid(), + service(PropertyWriteInfoExtractorInterface::class)->nullOnInvalid(), ]) ->alias(PropertyAccessorInterface::class, 'property_accessor') From 9c85df68bfc4b8b37635a772498b75c36ee84230 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 8 Jan 2025 08:31:12 +0100 Subject: [PATCH 019/111] rename test to match what's actually tested --- Tests/Functional/ContainerDebugCommandTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index b5d395a23..1037074c1 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -342,10 +342,10 @@ public static function provideCompletionSuggestions(): iterable ]; } - public function testShowArgumentsNotProvidedShouldTriggerDeprecation() + public function testShowArgumentsProvidedShouldTriggerDeprecation() { static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true]); - $path = sprintf('%s/%sDeprecations.log', static::$kernel->getContainer()->getParameter('kernel.build_dir'), static::$kernel->getContainer()->getParameter('kernel.container_class')); + $path = \sprintf('%s/%sDeprecations.log', static::$kernel->getContainer()->getParameter('kernel.build_dir'), static::$kernel->getContainer()->getParameter('kernel.container_class')); @unlink($path); $application = new Application(static::$kernel); From b6d4072a132dd50f57307a3598a18f8c7599646c Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Tue, 31 Dec 2024 13:49:42 -0500 Subject: [PATCH 020/111] Add support for invokable commands and input attributes --- DependencyInjection/FrameworkExtension.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 5a86acb54..38d37c7fc 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -49,6 +49,7 @@ use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Config\ResourceCheckerInterface; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\DataCollector\CommandDataCollector; use Symfony\Component\Console\Debug\CliRequest; @@ -608,6 +609,9 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('assets.package'); $container->registerForAutoconfiguration(AssetCompilerInterface::class) ->addTag('asset_mapper.compiler'); + $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass $reflector): void { + $definition->addTag('console.command', ['command' => $attribute->name, 'description' => $attribute->description ?? $reflector->getName()]); + }); $container->registerForAutoconfiguration(Command::class) ->addTag('console.command'); $container->registerForAutoconfiguration(ResourceCheckerInterface::class) From d76bcc720ef65fe22781fa2cd4c9b5a1f7e8a829 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 10 Jan 2025 15:17:09 +0100 Subject: [PATCH 021/111] chore: PHP CS Fixer fixes --- Command/TranslationExtractCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Command/TranslationExtractCommand.php b/Command/TranslationExtractCommand.php index c794b20d6..d7967bbe8 100644 --- a/Command/TranslationExtractCommand.php +++ b/Command/TranslationExtractCommand.php @@ -62,7 +62,7 @@ public function __construct( parent::__construct(); if (!method_exists($writer, 'getFormats')) { - throw new \InvalidArgumentException(sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class)); + throw new \InvalidArgumentException(\sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class)); } } From 271adae1e05979f3fdf516a883326b4ef6799f21 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 7 Jan 2025 15:21:25 +0100 Subject: [PATCH 022/111] [JsonEncoder] Add `JsonEncodable` attribute --- .../Compiler/UnusedTagsPass.php | 1 - DependencyInjection/FrameworkExtension.php | 5 ++++ Tests/Functional/JsonEncoderTest.php | 26 ++++++++++++++++--- .../Functional/app/JsonEncoder/Dto/Dummy.php | 2 ++ Tests/Functional/app/JsonEncoder/config.yml | 5 ++++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index a2a571f83..b6a4c8a7c 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -54,7 +54,6 @@ class UnusedTagsPass implements CompilerPassInterface 'html_sanitizer', 'http_client.client', 'json_encoder.denormalizer', - 'json_encoder.encodable', 'json_encoder.normalizer', 'kernel.cache_clearer', 'kernel.cache_warmer', diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 38d37c7fc..6d20ca653 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -100,6 +100,7 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; +use Symfony\Component\JsonEncoder\Attribute\JsonEncodable; use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface as JsonEncoderDenormalizerInterface; use Symfony\Component\JsonEncoder\DecoderInterface as JsonEncoderDecoderInterface; use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface as JsonEncoderNormalizerInterface; @@ -745,6 +746,10 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } ); } + $container->registerAttributeForAutoconfiguration(JsonEncodable::class, static function (ChildDefinition $definition): void { + $definition->addTag('json_encoder.encodable'); + $definition->addTag('container.excluded'); + }); if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers diff --git a/Tests/Functional/JsonEncoderTest.php b/Tests/Functional/JsonEncoderTest.php index 0ab66e6c1..93ca1fd6d 100644 --- a/Tests/Functional/JsonEncoderTest.php +++ b/Tests/Functional/JsonEncoderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\JsonEncoder\DecoderInterface; use Symfony\Component\JsonEncoder\EncoderInterface; use Symfony\Component\TypeInfo\Type; @@ -21,10 +22,13 @@ */ class JsonEncoderTest extends AbstractWebTestCase { - public function testEncode() + protected function setUp(): void { static::bootKernel(['test_case' => 'JsonEncoder']); + } + public function testEncode() + { /** @var EncoderInterface $encoder */ $encoder = static::getContainer()->get('json_encoder.encoder.alias'); @@ -33,8 +37,6 @@ public function testEncode() public function testDecode() { - static::bootKernel(['test_case' => 'JsonEncoder']); - /** @var DecoderInterface $decoder */ $decoder = static::getContainer()->get('json_encoder.decoder.alias'); @@ -44,4 +46,22 @@ public function testDecode() $this->assertEquals($expected, $decoder->decode('{"@name": "DUMMY", "range": "0..1"}', Type::object(Dummy::class))); } + + public function testWarmupEncodableClasses() + { + /** @var Filesystem $fs */ + $fs = static::getContainer()->get('filesystem'); + + $encodersDir = \sprintf('%s/json_encoder/encoder/', static::getContainer()->getParameter('kernel.cache_dir')); + + // clear already created encoders + if ($fs->exists($encodersDir)) { + $fs->remove($encodersDir); + } + + static::getContainer()->get('json_encoder.cache_warmer.encoder_decoder.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir')); + + $this->assertFileExists($encodersDir); + $this->assertCount(1, glob($encodersDir.'/*')); + } } diff --git a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php index 344b9d11c..8610de049 100644 --- a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php +++ b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php @@ -14,11 +14,13 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer; use Symfony\Component\JsonEncoder\Attribute\Denormalizer; use Symfony\Component\JsonEncoder\Attribute\EncodedName; +use Symfony\Component\JsonEncoder\Attribute\JsonEncodable; use Symfony\Component\JsonEncoder\Attribute\Normalizer; /** * @author Mathias Arlaud */ +#[JsonEncodable] class Dummy { #[EncodedName('@name')] diff --git a/Tests/Functional/app/JsonEncoder/config.yml b/Tests/Functional/app/JsonEncoder/config.yml index a92aa3969..13b68adef 100644 --- a/Tests/Functional/app/JsonEncoder/config.yml +++ b/Tests/Functional/app/JsonEncoder/config.yml @@ -18,4 +18,9 @@ services: alias: json_encoder.decoder public: true + json_encoder.cache_warmer.encoder_decoder.alias: + alias: .json_encoder.cache_warmer.encoder_decoder + public: true + + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy: ~ Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer: ~ From 2185b8c3659337c012699f0aae47c33e031c15d7 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 13 Jan 2025 23:30:21 +0100 Subject: [PATCH 023/111] [PropertyInfo] Move aliases under service definition --- Resources/config/property_info.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/config/property_info.php b/Resources/config/property_info.php index f45d6ce2b..505dda6f4 100644 --- a/Resources/config/property_info.php +++ b/Resources/config/property_info.php @@ -48,11 +48,11 @@ ->tag('property_info.access_extractor', ['priority' => -1000]) ->tag('property_info.initializable_extractor', ['priority' => -1000]) + ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') + ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') + ->set('property_info.constructor_extractor', ConstructorExtractor::class) ->args([[]]) ->tag('property_info.type_extractor', ['priority' => -999]) - - ->alias(PropertyReadInfoExtractorInterface::class, 'property_info.reflection_extractor') - ->alias(PropertyWriteInfoExtractorInterface::class, 'property_info.reflection_extractor') ; }; From 80ee7565588d8c8ef7bbc60276c4643a0ff33c31 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 20 Nov 2024 11:25:30 +0100 Subject: [PATCH 024/111] [RateLimiter] Add `RateLimiterFactoryInterface` --- CHANGELOG.md | 1 + Resources/config/rate_limiter.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fa33a3c..9c9aecbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add JsonEncoder services and configuration * Add new `framework.property_info.with_constructor_extractor` option to allow enabling or disabling the constructor extractor integration * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown + * Add `RateLimiterFactoryInterface` as an alias of the `limiter` service 7.2 --- diff --git a/Resources/config/rate_limiter.php b/Resources/config/rate_limiter.php index 727a1f636..90af4d758 100644 --- a/Resources/config/rate_limiter.php +++ b/Resources/config/rate_limiter.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\RateLimiter\RateLimiterFactory; +use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; return static function (ContainerConfigurator $container) { $container->services() @@ -27,4 +28,9 @@ null, ]) ; + + if (interface_exists(RateLimiterFactoryInterface::class)) { + $container->services() + ->alias(RateLimiterFactoryInterface::class, 'limiter'); + } }; From d9fb99fc9c0e1837167d997ef1821dc9d0086e28 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Mon, 13 Jan 2025 10:35:32 -0500 Subject: [PATCH 025/111] [Console] Invokable command adjustments --- DependencyInjection/FrameworkExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 38d37c7fc..deb3d6957 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -610,7 +610,7 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(AssetCompilerInterface::class) ->addTag('asset_mapper.compiler'); $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass $reflector): void { - $definition->addTag('console.command', ['command' => $attribute->name, 'description' => $attribute->description ?? $reflector->getName()]); + $definition->addTag('console.command', ['command' => $attribute->name, 'description' => $attribute->description]); }); $container->registerForAutoconfiguration(Command::class) ->addTag('console.command'); From 1d0686b29824e72d964cd4d6ebae9088756c053c Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Fri, 10 Jan 2025 15:50:54 -0500 Subject: [PATCH 026/111] [Console] Add broader support for command "help" definition --- DependencyInjection/FrameworkExtension.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index d435c83e0..9ef918161 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -611,7 +611,11 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(AssetCompilerInterface::class) ->addTag('asset_mapper.compiler'); $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass $reflector): void { - $definition->addTag('console.command', ['command' => $attribute->name, 'description' => $attribute->description]); + $definition->addTag('console.command', [ + 'command' => $attribute->name, + 'description' => $attribute->description, + 'help' => $attribute->help, + ]); }); $container->registerForAutoconfiguration(Command::class) ->addTag('console.command'); From 3b39194257a46b11451070ec8c9a359854b46b8e Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Fri, 17 Jan 2025 09:45:41 +0100 Subject: [PATCH 027/111] [JsonEncoder] Allow to warm up item and list --- DependencyInjection/FrameworkExtension.php | 7 +++++-- Tests/Functional/JsonEncoderTest.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 6d20ca653..3ef5de070 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -746,8 +746,11 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } ); } - $container->registerAttributeForAutoconfiguration(JsonEncodable::class, static function (ChildDefinition $definition): void { - $definition->addTag('json_encoder.encodable'); + $container->registerAttributeForAutoconfiguration(JsonEncodable::class, static function (ChildDefinition $definition, JsonEncodable $attribute): void { + $definition->addTag('json_encoder.encodable', [ + 'object' => $attribute->asObject, + 'list' => $attribute->asList, + ]); $definition->addTag('container.excluded'); }); diff --git a/Tests/Functional/JsonEncoderTest.php b/Tests/Functional/JsonEncoderTest.php index 93ca1fd6d..b5410e1e1 100644 --- a/Tests/Functional/JsonEncoderTest.php +++ b/Tests/Functional/JsonEncoderTest.php @@ -62,6 +62,6 @@ public function testWarmupEncodableClasses() static::getContainer()->get('json_encoder.cache_warmer.encoder_decoder.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir')); $this->assertFileExists($encodersDir); - $this->assertCount(1, glob($encodersDir.'/*')); + $this->assertCount(2, glob($encodersDir.'/*')); } } From 2f93db123f4efd5ea4c025dae91a5fe52dc088ae Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 27 Jan 2025 13:38:29 +0100 Subject: [PATCH 028/111] [FrameworkBundle] Add support for info on `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()` --- DependencyInjection/Configuration.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 265fba9fd..c765f22ce 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1054,12 +1054,8 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->end() ->arrayNode('not_compromised_password') - ->canBeDisabled() + ->canBeDisabled('When disabled, compromised passwords will be accepted as valid.') ->children() - ->booleanNode('enabled') - ->defaultTrue() - ->info('When disabled, compromised passwords will be accepted as valid.') - ->end() ->scalarNode('endpoint') ->defaultNull() ->info('API endpoint for the NotCompromisedPassword Validator.') From feda5a4301d8dd65ba4b7c3119155d480dfc6cef Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 29 Jan 2025 16:47:19 +0100 Subject: [PATCH 029/111] bump symfony/config requirement to 7.3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f4c482356..03707eea3 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "composer-runtime-api": ">=2.1", "ext-xml": "*", "symfony/cache": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", + "symfony/config": "^7.3", "symfony/dependency-injection": "^7.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", From 2f045752a53f1a154954bff13aa8b6019835998f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Feb 2025 13:02:23 +0100 Subject: [PATCH 030/111] fix package name in deprecation --- DependencyInjection/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index c765f22ce..33d84dd18 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1234,7 +1234,7 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable ->then(function ($v) { $v['property_info']['with_constructor_extractor'] = false; - trigger_deprecation('symfony/property-info', '7.3', 'Not setting the "with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); + trigger_deprecation('symfony/framework-bundle', '7.3', 'Not setting the "with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); return $v; }) From 4abc95f58ee6069fe4c0870f07646843ac9d68be Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Feb 2025 21:58:22 +0100 Subject: [PATCH 031/111] fix compat with AsCommand attributes from symfony/console < 7.3 FrameworkBundle 7.3 can be used with older releases of the Console component which do not provide help information through the AsCommand attribute. --- DependencyInjection/FrameworkExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 88fe69ec3..f6160d493 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -610,11 +610,11 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('assets.package'); $container->registerForAutoconfiguration(AssetCompilerInterface::class) ->addTag('asset_mapper.compiler'); - $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass $reflector): void { + $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute): void { $definition->addTag('console.command', [ 'command' => $attribute->name, 'description' => $attribute->description, - 'help' => $attribute->help, + 'help' => $attribute->help ?? null, ]); }); $container->registerForAutoconfiguration(Command::class) From 24b0b4e35b862ad3ffdc9af6201fdac289548564 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 3 Feb 2025 11:05:40 +0100 Subject: [PATCH 032/111] Deduplicate Middleware --- DependencyInjection/FrameworkExtension.php | 8 +++ Resources/config/messenger.php | 6 ++ .../FrameworkExtensionTestCase.php | 64 ++++++++++++++----- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f6160d493..83f9011ac 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -120,6 +120,7 @@ use Symfony\Component\Messenger\Handler\BatchHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Middleware\DeduplicateMiddleware; use Symfony\Component\Messenger\Middleware\RouterContextMiddleware; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface as MessengerTransportFactoryInterface; @@ -2266,6 +2267,13 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ['id' => 'handle_message'], ], ]; + + if (class_exists(DeduplicateMiddleware::class) && class_exists(LockFactory::class)) { + $defaultMiddleware['before'][] = ['id' => 'deduplicate_middleware']; + } else { + $container->removeDefinition('messenger.middleware.deduplicate_middleware'); + } + foreach ($config['buses'] as $busId => $bus) { $middleware = $bus['middleware']; diff --git a/Resources/config/messenger.php b/Resources/config/messenger.php index 40f5b84ca..8798d5f2e 100644 --- a/Resources/config/messenger.php +++ b/Resources/config/messenger.php @@ -25,6 +25,7 @@ use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener; use Symfony\Component\Messenger\Handler\RedispatchMessageHandler; use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware; +use Symfony\Component\Messenger\Middleware\DeduplicateMiddleware; use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware; use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; @@ -86,6 +87,11 @@ ->tag('monolog.logger', ['channel' => 'messenger']) ->call('setLogger', [service('logger')->ignoreOnInvalid()]) + ->set('messenger.middleware.deduplicate_middleware', DeduplicateMiddleware::class) + ->args([ + service('lock.factory'), + ]) + ->set('messenger.middleware.add_bus_name_stamp_middleware', AddBusNameStampMiddleware::class) ->abstract() diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 1f4daac5d..2535707f5 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -62,6 +62,7 @@ use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory; use Symfony\Component\Messenger\Bridge\Beanstalkd\Transport\BeanstalkdTransportFactory; use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; +use Symfony\Component\Messenger\Middleware\DeduplicateMiddleware; use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\Notifier\ChatterInterface; use Symfony\Component\Notifier\TexterInterface; @@ -1061,25 +1062,54 @@ public function testMessengerWithMultipleBuses() $this->assertTrue($container->has('messenger.bus.commands')); $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); - $this->assertEquals([ - ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], - ['id' => 'reject_redelivered_message_middleware'], - ['id' => 'dispatch_after_current_bus'], - ['id' => 'failed_message_processing_middleware'], - ['id' => 'send_message', 'arguments' => [true]], - ['id' => 'handle_message', 'arguments' => [false]], - ], $container->getParameter('messenger.bus.commands.middleware')); + + if (class_exists(DeduplicateMiddleware::class)) { + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'deduplicate_middleware'], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.commands.middleware')); + } else { + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.commands.middleware')); + } + $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); - $this->assertEquals([ - ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], - ['id' => 'reject_redelivered_message_middleware'], - ['id' => 'dispatch_after_current_bus'], - ['id' => 'failed_message_processing_middleware'], - ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], - ['id' => 'send_message', 'arguments' => [true]], - ['id' => 'handle_message', 'arguments' => [false]], - ], $container->getParameter('messenger.bus.events.middleware')); + + if (class_exists(DeduplicateMiddleware::class)) { + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'deduplicate_middleware'], + ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.events.middleware')); + } else { + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.events.middleware')); + } + $this->assertTrue($container->has('messenger.bus.queries')); $this->assertSame([], $container->getDefinition('messenger.bus.queries')->getArgument(0)); $this->assertEquals([ From ac168b622265da8e9ebe029c8c0aa1fd390fb7fe Mon Sep 17 00:00:00 2001 From: valtzu Date: Sat, 1 Feb 2025 18:47:33 +0200 Subject: [PATCH 033/111] Add `NumberNormalizer` --- DependencyInjection/FrameworkExtension.php | 6 ++++++ Resources/config/serializer.php | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f6160d493..51699f298 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -170,6 +170,7 @@ use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NumberNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\String\LazyString; @@ -1933,6 +1934,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.normalizer.mime_message'); } + // BC layer Serializer < 7.3 + if (!class_exists(NumberNormalizer::class)) { + $container->removeDefinition('serializer.normalizer.number'); + } + // BC layer Serializer < 7.2 if (!class_exists(SnakeCaseToCamelCaseNameConverter::class)) { $container->removeDefinition('serializer.name_converter.snake_case_to_camel_case'); diff --git a/Resources/config/serializer.php b/Resources/config/serializer.php index 4686a88f6..01b9a3eb0 100644 --- a/Resources/config/serializer.php +++ b/Resources/config/serializer.php @@ -44,6 +44,7 @@ use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NumberNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; @@ -221,5 +222,8 @@ ->set('serializer.normalizer.backed_enum', BackedEnumNormalizer::class) ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -915]) + + ->set('serializer.normalizer.number', NumberNormalizer::class) + ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -915]) ; }; From 94e246ba9183b8910debde9b380b335a3f8ac6be Mon Sep 17 00:00:00 2001 From: valtzu Date: Mon, 3 Feb 2025 17:55:58 +0200 Subject: [PATCH 034/111] Normalize `TriggerInterface` as `string` --- Resources/config/scheduler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Resources/config/scheduler.php b/Resources/config/scheduler.php index 7b2856d82..4cbfb73b5 100644 --- a/Resources/config/scheduler.php +++ b/Resources/config/scheduler.php @@ -13,6 +13,7 @@ use Symfony\Component\Scheduler\EventListener\DispatchSchedulerEventListener; use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory; +use Symfony\Component\Scheduler\Messenger\Serializer\Normalizer\SchedulerTriggerNormalizer; use Symfony\Component\Scheduler\Messenger\ServiceCallMessageHandler; return static function (ContainerConfigurator $container) { @@ -34,5 +35,7 @@ service('event_dispatcher'), ]) ->tag('kernel.event_subscriber') + ->set('serializer.normalizer.scheduler_trigger', SchedulerTriggerNormalizer::class) + ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -880]) ; }; From 56807dbf418106639b418c46e3562b12fdb90088 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 27 Jun 2023 16:01:55 +0200 Subject: [PATCH 035/111] [FrameworkBundle][Validator] Add `framework.validation.disable_translation` config --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 3 +++ DependencyInjection/FrameworkExtension.php | 4 ++++ Resources/config/schema/symfony-1.0.xsd | 1 + Tests/DependencyInjection/ConfigurationTest.php | 1 + 5 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9aecbd5..49f8d634d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add new `framework.property_info.with_constructor_extractor` option to allow enabling or disabling the constructor extractor integration * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown * Add `RateLimiterFactoryInterface` as an alias of the `limiter` service + * Add `framework.validation.disable_translation` option 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 33d84dd18..7f77b4941 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1062,6 +1062,9 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->end() ->end() + ->booleanNode('disable_translation') + ->defaultFalse() + ->end() ->arrayNode('auto_mapping') ->info('A collection of namespaces for which auto-mapping will be enabled by default, or null to opt-in with the EnableAutoMapping constraint.') ->example([ diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index bd04b0dbf..c1b777ad8 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1724,6 +1724,10 @@ private function registerValidationConfiguration(array $config, ContainerBuilder $validatorBuilder->addMethodCall('setMappingCache', [new Reference('validator.mapping.cache.adapter')]); } + if ($config['disable_translation'] ?? false) { + $validatorBuilder->addMethodCall('disableTranslation'); + } + $container->setParameter('validator.auto_mapping', $config['auto_mapping']); if (!$propertyInfoEnabled || !class_exists(PropertyInfoLoader::class)) { $container->removeDefinition('validator.property_info_loader'); diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 0f90f1cfc..41c5d8557 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -298,6 +298,7 @@ + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index e8ca9873f..4ede2c07e 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -775,6 +775,7 @@ protected static function getBundleDefaultConfig() 'enable_attributes' => !class_exists(FullStack::class), 'static_method' => ['loadValidatorMetadata'], 'translation_domain' => 'validators', + 'disable_translation' => false, 'mapping' => [ 'paths' => [], ], From 4bd05f1d17826f74d3bdd777794c5921bce23c1f Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 7 Feb 2025 14:16:34 +0100 Subject: [PATCH 036/111] fix compatibility with symfony/scheduler < 7.3 --- DependencyInjection/FrameworkExtension.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index c1b777ad8..10d703744 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -156,6 +156,7 @@ use Symfony\Component\Scheduler\Attribute\AsPeriodicTask; use Symfony\Component\Scheduler\Attribute\AsSchedule; use Symfony\Component\Scheduler\Messenger\SchedulerTransportFactory; +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\CsrfTokenManagerInterface; @@ -2220,6 +2221,11 @@ private function registerSchedulerConfiguration(ContainerBuilder $container, Php if (!$this->hasConsole()) { $container->removeDefinition('console.command.scheduler_debug'); } + + // BC layer Scheduler < 7.3 + if (!class_exists(SchedulerTriggerNormalizer::class)) { + $container->removeDefinition('serializer.normalizer.scheduler_trigger'); + } } private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $validationEnabled): void From 889f88f81f1b405b700fe6200ba455e8ce87f4e6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 8 Feb 2025 15:39:35 +0100 Subject: [PATCH 037/111] the DeduplicateMiddleware requires the lock feature to be enabled If the `lock` feature is not enabled, no `lock.factory` service will be registered. --- DependencyInjection/FrameworkExtension.php | 6 +- ...iple_buses_with_deduplicate_middleware.php | 27 +++++ ..._buses_without_deduplicate_middleware.php} | 0 ...iple_buses_with_deduplicate_middleware.xml | 29 +++++ ..._buses_without_deduplicate_middleware.xml} | 0 ...iple_buses_with_deduplicate_middleware.yml | 19 ++++ ..._buses_without_deduplicate_middleware.yml} | 0 .../FrameworkExtensionTestCase.php | 103 ++++++++++-------- 8 files changed, 136 insertions(+), 48 deletions(-) create mode 100644 Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php rename Tests/DependencyInjection/Fixtures/php/{messenger_multiple_buses.php => messenger_multiple_buses_without_deduplicate_middleware.php} (100%) create mode 100644 Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml rename Tests/DependencyInjection/Fixtures/xml/{messenger_multiple_buses.xml => messenger_multiple_buses_without_deduplicate_middleware.xml} (100%) create mode 100644 Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml rename Tests/DependencyInjection/Fixtures/yml/{messenger_multiple_buses.yml => messenger_multiple_buses_without_deduplicate_middleware.yml} (100%) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 10d703744..96bd5924b 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -538,7 +538,7 @@ public function load(array $configs, ContainerBuilder $container): void // messenger depends on validation being registered if ($messengerEnabled) { - $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $this->readConfigEnabled('validation', $container, $config['validation'])); + $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $this->readConfigEnabled('validation', $container, $config['validation']), $this->readConfigEnabled('lock', $container, $config['lock'])); } else { $container->removeDefinition('console.command.messenger_consume_messages'); $container->removeDefinition('console.command.messenger_stats'); @@ -2228,7 +2228,7 @@ private function registerSchedulerConfiguration(ContainerBuilder $container, Php } } - private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $validationEnabled): void + private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, bool $validationEnabled, bool $lockEnabled): void { if (!interface_exists(MessageBusInterface::class)) { throw new LogicException('Messenger support cannot be enabled as the Messenger component is not installed. Try running "composer require symfony/messenger".'); @@ -2284,7 +2284,7 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder ], ]; - if (class_exists(DeduplicateMiddleware::class) && class_exists(LockFactory::class)) { + if ($lockEnabled && class_exists(DeduplicateMiddleware::class) && class_exists(LockFactory::class)) { $defaultMiddleware['before'][] = ['id' => 'deduplicate_middleware']; } else { $container->removeDefinition('messenger.middleware.deduplicate_middleware'); diff --git a/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php new file mode 100644 index 000000000..f9b3767c0 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php @@ -0,0 +1,27 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'lock' => null, + 'messenger' => [ + 'default_bus' => 'messenger.bus.commands', + 'buses' => [ + 'messenger.bus.commands' => null, + 'messenger.bus.events' => [ + 'middleware' => [ + ['with_factory' => ['foo', true, ['bar' => 'baz']]], + ], + ], + 'messenger.bus.queries' => [ + 'default_middleware' => false, + 'middleware' => [ + 'send_message', + 'handle_message', + ], + ], + ], + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php similarity index 100% rename from Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php rename to Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php diff --git a/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml new file mode 100644 index 000000000..67decad20 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + foo + true + + baz + + + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml similarity index 100% rename from Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml rename to Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml diff --git a/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml new file mode 100644 index 000000000..ed52564c7 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml @@ -0,0 +1,19 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + lock: ~ + messenger: + default_bus: messenger.bus.commands + buses: + messenger.bus.commands: ~ + messenger.bus.events: + middleware: + - with_factory: [foo, true, { bar: baz }] + messenger.bus.queries: + default_middleware: false + middleware: + - "send_message" + - "handle_message" diff --git a/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml similarity index 100% rename from Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml rename to Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 2535707f5..a50070858 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1056,60 +1056,73 @@ public function testMessengerTransportConfiguration() $this->assertSame(['enable_max_depth' => true], $serializerTransportDefinition->getArgument(2)); } - public function testMessengerWithMultipleBuses() + public function testMessengerWithMultipleBusesWithoutDeduplicateMiddleware() { - $container = $this->createContainerFromFile('messenger_multiple_buses'); + $container = $this->createContainerFromFile('messenger_multiple_buses_without_deduplicate_middleware'); $this->assertTrue($container->has('messenger.bus.commands')); $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); - - if (class_exists(DeduplicateMiddleware::class)) { - $this->assertEquals([ - ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], - ['id' => 'reject_redelivered_message_middleware'], - ['id' => 'dispatch_after_current_bus'], - ['id' => 'failed_message_processing_middleware'], - ['id' => 'deduplicate_middleware'], - ['id' => 'send_message', 'arguments' => [true]], - ['id' => 'handle_message', 'arguments' => [false]], - ], $container->getParameter('messenger.bus.commands.middleware')); - } else { - $this->assertEquals([ - ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], - ['id' => 'reject_redelivered_message_middleware'], - ['id' => 'dispatch_after_current_bus'], - ['id' => 'failed_message_processing_middleware'], - ['id' => 'send_message', 'arguments' => [true]], - ['id' => 'handle_message', 'arguments' => [false]], - ], $container->getParameter('messenger.bus.commands.middleware')); - } - + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.commands.middleware')); $this->assertTrue($container->has('messenger.bus.events')); $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.events.middleware')); + $this->assertTrue($container->has('messenger.bus.queries')); + $this->assertSame([], $container->getDefinition('messenger.bus.queries')->getArgument(0)); + $this->assertEquals([ + ['id' => 'send_message', 'arguments' => []], + ['id' => 'handle_message', 'arguments' => []], + ], $container->getParameter('messenger.bus.queries.middleware')); - if (class_exists(DeduplicateMiddleware::class)) { - $this->assertEquals([ - ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], - ['id' => 'reject_redelivered_message_middleware'], - ['id' => 'dispatch_after_current_bus'], - ['id' => 'failed_message_processing_middleware'], - ['id' => 'deduplicate_middleware'], - ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], - ['id' => 'send_message', 'arguments' => [true]], - ['id' => 'handle_message', 'arguments' => [false]], - ], $container->getParameter('messenger.bus.events.middleware')); - } else { - $this->assertEquals([ - ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], - ['id' => 'reject_redelivered_message_middleware'], - ['id' => 'dispatch_after_current_bus'], - ['id' => 'failed_message_processing_middleware'], - ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], - ['id' => 'send_message', 'arguments' => [true]], - ['id' => 'handle_message', 'arguments' => [false]], - ], $container->getParameter('messenger.bus.events.middleware')); + $this->assertTrue($container->hasAlias('messenger.default_bus')); + $this->assertSame('messenger.bus.commands', (string) $container->getAlias('messenger.default_bus')); + } + + public function testMessengerWithMultipleBusesWithDeduplicateMiddleware() + { + if (!class_exists(DeduplicateMiddleware::class)) { + $this->markTestSkipped('DeduplicateMiddleware not available.'); } + $container = $this->createContainerFromFile('messenger_multiple_buses_with_deduplicate_middleware'); + + $this->assertTrue($container->has('messenger.bus.commands')); + $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'deduplicate_middleware'], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.commands.middleware')); + $this->assertTrue($container->has('messenger.bus.events')); + $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); + $this->assertEquals([ + ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], + ['id' => 'reject_redelivered_message_middleware'], + ['id' => 'dispatch_after_current_bus'], + ['id' => 'failed_message_processing_middleware'], + ['id' => 'deduplicate_middleware'], + ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], + ['id' => 'send_message', 'arguments' => [true]], + ['id' => 'handle_message', 'arguments' => [false]], + ], $container->getParameter('messenger.bus.events.middleware')); $this->assertTrue($container->has('messenger.bus.queries')); $this->assertSame([], $container->getDefinition('messenger.bus.queries')->getArgument(0)); $this->assertEquals([ From 6ca24561727edcb5e251a04bf73508fdd8165c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?El=C3=ADas=20Fern=C3=A1ndez?= Date: Tue, 8 Oct 2024 18:14:29 +0200 Subject: [PATCH 038/111] [Mailer] Add configuration for dkim and smime signers --- DependencyInjection/Configuration.php | 82 +++++++++++++++++++ DependencyInjection/FrameworkExtension.php | 45 ++++++++++ Resources/config/mailer.php | 48 +++++++++++ Resources/config/schema/symfony-1.0.xsd | 27 ++++++ .../DependencyInjection/ConfigurationTest.php | 21 +++++ 5 files changed, 223 insertions(+) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 33d84dd18..04e747bc7 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2236,6 +2236,88 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->end() ->end() ->end() + ->arrayNode('dkim_signer') + ->addDefaultsIfNotSet() + ->fixXmlConfig('option') + ->canBeEnabled() + ->info('DKIM signer configuration') + ->children() + ->scalarNode('key') + ->info('Key content, or path to key (in PEM format with the `file://` prefix)') + ->defaultValue('') + ->cannotBeEmpty() + ->end() + ->scalarNode('domain')->defaultValue('')->end() + ->scalarNode('select')->defaultValue('')->end() + ->scalarNode('passphrase') + ->info('The private key passphrase') + ->defaultValue('') + ->end() + ->arrayNode('options') + ->performNoDeepMerging() + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->prototype('variable')->end() + ->end() + ->end() + ->end() + ->arrayNode('smime_signer') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->info('S/MIME signer configuration') + ->children() + ->scalarNode('key') + ->info('Path to key (in PEM format)') + ->defaultValue('') + ->cannotBeEmpty() + ->end() + ->scalarNode('certificate') + ->info('Path to certificate (in PEM format without the `file://` prefix)') + ->defaultValue('') + ->cannotBeEmpty() + ->end() + ->scalarNode('passphrase') + ->info('The private key passphrase') + ->defaultNull() + ->end() + ->scalarNode('extra_certificates')->defaultNull()->end() + ->integerNode('sign_options')->defaultNull()->end() + ->end() + ->end() + ->arrayNode('smime_encrypter') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->info('S/MIME encrypter configuration') + ->children() + ->scalarNode('certificate') + ->info('Path to certificate (in PEM format without the `file://` prefix)') + ->defaultValue('') + ->cannotBeEmpty() + ->end() + ->integerNode('cipher') + ->info('A set of algorithms used to encrypt the message') + ->defaultNull() + ->beforeNormalization() + ->always(function ($v): ?int { + if (null === $v) { + return null; + } + if (\defined('OPENSSL_CIPHER_'.$v)) { + return \constant('OPENSSL_CIPHER_'.$v); + } + + throw new \InvalidArgumentException(\sprintf('"%s" is not a valid OPENSSL cipher.', $v)); + }) + ->end() + ->validate() + ->ifTrue(function ($v) { + return \extension_loaded('openssl') && null !== $v && !\defined('OPENSSL_CIPHER_'.$v); + }) + ->thenInvalid('You must provide a valid cipher.') + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f6160d493..56cba363b 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -112,7 +112,10 @@ use Symfony\Component\Lock\Store\StoreFactory; use Symfony\Component\Mailer\Bridge as MailerBridge; use Symfony\Component\Mailer\Command\MailerTestCommand; +use Symfony\Component\Mailer\EventListener\DkimSignedMessageListener; use Symfony\Component\Mailer\EventListener\MessengerTransportListener; +use Symfony\Component\Mailer\EventListener\SmimeEncryptedMessageListener; +use Symfony\Component\Mailer\EventListener\SmimeSignedMessageListener; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mercure\HubRegistry; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -2837,6 +2840,48 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $container->removeDefinition('mailer.messenger_transport_listener'); } + if ($config['dkim_signer']['enabled']) { + if (!class_exists(DkimSignedMessageListener::class)) { + throw new LogicException('DKIM signed messages support cannot be enabled as this version of the Mailer component does not support it.'); + } + $dkimSigner = $container->getDefinition('mailer.dkim_signer'); + $dkimSigner->setArgument(0, $config['dkim_signer']['key']); + $dkimSigner->setArgument(1, $config['dkim_signer']['domain']); + $dkimSigner->setArgument(2, $config['dkim_signer']['select']); + $dkimSigner->setArgument(3, $config['dkim_signer']['options']); + $dkimSigner->setArgument(4, $config['dkim_signer']['passphrase']); + } else { + $container->removeDefinition('mailer.dkim_signer'); + $container->removeDefinition('mailer.dkim_signer.listener'); + } + + if ($config['smime_signer']['enabled']) { + if (!class_exists(SmimeSignedMessageListener::class)) { + throw new LogicException('SMIME signed messages support cannot be enabled as this version of the Mailer component does not support it.'); + } + $smimeSigner = $container->getDefinition('mailer.smime_signer'); + $smimeSigner->setArgument(0, $config['smime_signer']['key']); + $smimeSigner->setArgument(1, $config['smime_signer']['certificate']); + $smimeSigner->setArgument(2, $config['smime_signer']['passphrase']); + $smimeSigner->setArgument(3, $config['smime_signer']['extra_certificates']); + $smimeSigner->setArgument(4, $config['smime_signer']['sign_options']); + } else { + $container->removeDefinition('mailer.smime_signer'); + $container->removeDefinition('mailer.smime_signer.listener'); + } + + if ($config['smime_encrypter']['enabled']) { + if (!class_exists(SmimeEncryptedMessageListener::class)) { + throw new LogicException('S/MIME encrypted messages support cannot be enabled as this version of the Mailer component does not support it.'); + } + $smimeDecrypter = $container->getDefinition('mailer.smime_encrypter'); + $smimeDecrypter->setArgument(0, $config['smime_encrypter']['certificate']); + $smimeDecrypter->setArgument(1, $config['smime_encrypter']['cipher']); + } else { + $container->removeDefinition('mailer.smime_encrypter'); + $container->removeDefinition('mailer.smime_encrypter.listener'); + } + if ($webhookEnabled) { $loader->load('mailer_webhook.php'); } diff --git a/Resources/config/mailer.php b/Resources/config/mailer.php index 9eb545ca2..25b3fefdb 100644 --- a/Resources/config/mailer.php +++ b/Resources/config/mailer.php @@ -12,16 +12,22 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Mailer\Command\MailerTestCommand; +use Symfony\Component\Mailer\EventListener\DkimSignedMessageListener; use Symfony\Component\Mailer\EventListener\EnvelopeListener; use Symfony\Component\Mailer\EventListener\MessageListener; use Symfony\Component\Mailer\EventListener\MessageLoggerListener; use Symfony\Component\Mailer\EventListener\MessengerTransportListener; +use Symfony\Component\Mailer\EventListener\SmimeEncryptedMessageListener; +use Symfony\Component\Mailer\EventListener\SmimeSignedMessageListener; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\Messenger\MessageHandler; use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\Transports; +use Symfony\Component\Mime\Crypto\DkimSigner; +use Symfony\Component\Mime\Crypto\SMimeEncrypter; +use Symfony\Component\Mime\Crypto\SMimeSigner; return static function (ContainerConfigurator $container) { $container->services() @@ -78,6 +84,48 @@ ->set('mailer.messenger_transport_listener', MessengerTransportListener::class) ->tag('kernel.event_subscriber') + ->set('mailer.dkim_signer', DkimSigner::class) + ->args([ + abstract_arg('key'), + abstract_arg('domain'), + abstract_arg('select'), + abstract_arg('options'), + abstract_arg('passphrase'), + ]) + + ->set('mailer.smime_signer', SMimeSigner::class) + ->args([ + abstract_arg('key'), + abstract_arg('certificate'), + abstract_arg('passphrase'), + abstract_arg('extraCertificates'), + abstract_arg('signOptions'), + ]) + + ->set('mailer.smime_encrypter', SMimeEncrypter::class) + ->args([ + abstract_arg('certificate'), + abstract_arg('cipher'), + ]) + + ->set('mailer.dkim_signer.listener', DkimSignedMessageListener::class) + ->args([ + service(DkimSigner::class), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.smime_signer.listener', SmimeSignedMessageListener::class) + ->args([ + service('mailer.smime_signer'), + ]) + ->tag('kernel.event_subscriber') + + ->set('mailer.smime_encrypter.listener', SmimeEncryptedMessageListener::class) + ->args([ + service('mailer.smime_encrypter'), + ]) + ->tag('kernel.event_subscriber') + ->set('console.command.mailer_test', MailerTestCommand::class) ->args([ service('mailer.transports'), diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 0f90f1cfc..e2edb6aa0 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -789,6 +789,9 @@ + + + @@ -811,6 +814,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index e8ca9873f..69557cc8f 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -922,6 +922,27 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class), 'message_bus' => null, 'headers' => [], + 'dkim_signer' => [ + 'enabled' => false, + 'options' => [], + 'key' => '', + 'domain' => '', + 'select' => '', + 'passphrase' => '', + ], + 'smime_signer' => [ + 'enabled' => false, + 'key' => '', + 'certificate' => '', + 'passphrase' => null, + 'extra_certificates' => null, + 'sign_options' => null, + ], + 'smime_encrypter' => [ + 'enabled' => false, + 'certificate' => '', + 'cipher' => null, + ], ], 'notifier' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(Notifier::class), From 582d165582e05480e8a9a5626086f1af6596af95 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 20 Mar 2023 22:09:31 +0100 Subject: [PATCH 039/111] [FrameworkBundle] Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 22 +++++++++++++++++++++- Resources/config/schema/symfony-1.0.xsd | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f8d634d..55eeaf1fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown * Add `RateLimiterFactoryInterface` as an alias of the `limiter` service * Add `framework.validation.disable_translation` option + * Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 7b1cafac5..f5279c419 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1572,6 +1572,7 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode, callable $en ->{$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.') @@ -1704,7 +1705,26 @@ function ($a) { ->arrayNode('stop_worker_on_signals') ->defaultValue([]) ->info('A list of signals that should stop the worker; defaults to SIGTERM and SIGINT.') - ->integerPrototype()->end() + ->beforeNormalization() + ->always(function ($signals) { + if (!\is_array($signals)) { + throw new InvalidConfigurationException('The "stop_worker_on_signals" option must be an array in messenger configuration.'); + } + + 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); + } + + if (!\is_int($v)) { + throw new InvalidConfigurationException('The "stop_worker_on_signals" option must be an array of pcntl signals in messenger configuration.'); + } + + return $v; + }, $signals); + }) + ->end() + ->scalarPrototype()->end() ->end() ->scalarNode('default_bus')->defaultNull()->end() ->arrayNode('buses') diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 2dc46beb9..b47de331a 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -609,6 +609,7 @@ + From b9fbf4e0fc27ff441b0d3d0f07b478d10cfb0241 Mon Sep 17 00:00:00 2001 From: alanzarli Date: Tue, 21 Jan 2025 10:32:54 +0100 Subject: [PATCH 040/111] [Notifier][Webhook] Add Smsbox 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 bc1d623e7..bff1fcfda 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -3117,6 +3117,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $loader->load('notifier_webhook.php'); $webhookRequestParsers = [ + 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', NotifierBridge\Vonage\Webhook\VonageRequestParser::class => 'notifier.webhook.request_parser.vonage', diff --git a/Resources/config/notifier_webhook.php b/Resources/config/notifier_webhook.php index 6447f4139..0b30c33e2 100644 --- a/Resources/config/notifier_webhook.php +++ b/Resources/config/notifier_webhook.php @@ -11,12 +11,16 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Notifier\Bridge\Smsbox\Webhook\SmsboxRequestParser; use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser; use Symfony\Component\Notifier\Bridge\Twilio\Webhook\TwilioRequestParser; use Symfony\Component\Notifier\Bridge\Vonage\Webhook\VonageRequestParser; return static function (ContainerConfigurator $container) { $container->services() + ->set('notifier.webhook.request_parser.smsbox', SmsboxRequestParser::class) + ->alias(SmsboxRequestParser::class, 'notifier.webhook.request_parser.smsbox') + ->set('notifier.webhook.request_parser.sweego', SweegoRequestParser::class) ->alias(SweegoRequestParser::class, 'notifier.webhook.request_parser.sweego') From e7ba416984e0b5c53e9bc3d4b4a3245b12803ff3 Mon Sep 17 00:00:00 2001 From: Frank Schulze Date: Mon, 6 Jan 2025 18:49:38 +0100 Subject: [PATCH 041/111] [Notifier] Add Matrix bridge --- DependencyInjection/FrameworkExtension.php | 1 + Resources/config/notifier_transports.php | 1 + 2 files changed, 2 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 38d37c7fc..24f84d327 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2926,6 +2926,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\Lox24\Lox24TransportFactory::class => 'notifier.transport_factory.lox24', NotifierBridge\Mailjet\MailjetTransportFactory::class => 'notifier.transport_factory.mailjet', NotifierBridge\Mastodon\MastodonTransportFactory::class => 'notifier.transport_factory.mastodon', + NotifierBridge\Matrix\MatrixTransportFactory::class => 'notifier.transport_factory.matrix', NotifierBridge\Mattermost\MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', NotifierBridge\Mercure\MercureTransportFactory::class => 'notifier.transport_factory.mercure', NotifierBridge\MessageBird\MessageBirdTransportFactory::class => 'notifier.transport_factory.message-bird', diff --git a/Resources/config/notifier_transports.php b/Resources/config/notifier_transports.php index f28007dec..d1adcfc37 100644 --- a/Resources/config/notifier_transports.php +++ b/Resources/config/notifier_transports.php @@ -36,6 +36,7 @@ 'line-notify' => Bridge\LineNotify\LineNotifyTransportFactory::class, 'linked-in' => Bridge\LinkedIn\LinkedInTransportFactory::class, 'mastodon' => Bridge\Mastodon\MastodonTransportFactory::class, + 'matrix' => Bridge\Matrix\MatrixTransportFactory::class, 'mattermost' => Bridge\Mattermost\MattermostTransportFactory::class, 'mercure' => Bridge\Mercure\MercureTransportFactory::class, 'microsoft-teams' => Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class, From bb860bf829832283e1e382f42dd60809e38ec508 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 27 Aug 2024 19:49:01 +0200 Subject: [PATCH 042/111] [Security] Add ability for voters to explain their vote --- Controller/AbstractController.php | 38 ++++++++++++++++++--- Tests/Controller/AbstractControllerTest.php | 28 +++++++++++++-- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Controller/AbstractController.php b/Controller/AbstractController.php index af453619b..de7395d5a 100644 --- a/Controller/AbstractController.php +++ b/Controller/AbstractController.php @@ -35,6 +35,7 @@ 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; @@ -202,6 +203,21 @@ protected function isGranted(mixed $attribute, mixed $subject = null): bool 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. + */ + protected 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. @@ -210,12 +226,24 @@ protected function isGranted(mixed $attribute, mixed $subject = null): bool */ protected function denyAccessUnlessGranted(mixed $attribute, mixed $subject = null, string $message = 'Access Denied.'): void { - if (!$this->isGranted($attribute, $subject)) { - $exception = $this->createAccessDeniedException($message); - $exception->setAttributes([$attribute]); - $exception->setSubject($subject); + 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 $exception; + throw $e; } } diff --git a/Tests/Controller/AbstractControllerTest.php b/Tests/Controller/AbstractControllerTest.php index 55a363984..5f5fc5ca5 100644 --- a/Tests/Controller/AbstractControllerTest.php +++ b/Tests/Controller/AbstractControllerTest.php @@ -40,7 +40,10 @@ use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Authorization\AccessDecision; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Vote; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; @@ -352,7 +355,19 @@ public function testIsGranted() public function testdenyAccessUnlessGranted() { $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); - $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(false); + $authorizationChecker + ->expects($this->once()) + ->method('isGranted') + ->willReturnCallback(function ($attribute, $subject, ?AccessDecision $accessDecision = null) { + if (class_exists(AccessDecision::class)) { + $this->assertInstanceOf(AccessDecision::class, $accessDecision); + $accessDecision->votes[] = $vote = new Vote(); + $vote->result = VoterInterface::ACCESS_DENIED; + $vote->reasons[] = 'Why should I.'; + } + + return false; + }); $container = new Container(); $container->set('security.authorization_checker', $authorizationChecker); @@ -361,8 +376,17 @@ public function testdenyAccessUnlessGranted() $controller->setContainer($container); $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Access Denied.'.(class_exists(AccessDecision::class) ? ' Why should I.' : '')); - $controller->denyAccessUnlessGranted('foo'); + try { + $controller->denyAccessUnlessGranted('foo'); + } catch (AccessDeniedException $e) { + if (class_exists(AccessDecision::class)) { + $this->assertFalse($e->getAccessDecision()->isGranted); + } + + throw $e; + } } /** From 8951bc81562bbe7cbe112e6c24aa1f9cf3818cae Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 23 Dec 2024 15:26:31 +0100 Subject: [PATCH 043/111] [JsonEncoder] Replace normalizers by value transformers --- .../Compiler/UnusedTagsPass.php | 3 +- DependencyInjection/FrameworkExtension.php | 9 ++---- Resources/config/json_encoder.php | 31 +++++++----------- .../Functional/app/JsonEncoder/Dto/Dummy.php | 12 +++---- .../RangeToStringValueTransformer.php | 32 +++++++++++++++++++ ....php => StringToRangeValueTransformer.php} | 16 +++------- Tests/Functional/app/JsonEncoder/config.yml | 3 +- 7 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php rename Tests/Functional/app/JsonEncoder/{RangeNormalizer.php => StringToRangeValueTransformer.php} (51%) diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 5714f8fc2..c5191a659 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -53,8 +53,7 @@ class UnusedTagsPass implements CompilerPassInterface 'form.type_guesser', 'html_sanitizer', 'http_client.client', - 'json_encoder.denormalizer', - 'json_encoder.normalizer', + 'json_encoder.value_transformer', 'kernel.cache_clearer', 'kernel.cache_warmer', 'kernel.event_listener', diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 88fe69ec3..ee302a20c 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -101,11 +101,10 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; use Symfony\Component\JsonEncoder\Attribute\JsonEncodable; -use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface as JsonEncoderDenormalizerInterface; use Symfony\Component\JsonEncoder\DecoderInterface as JsonEncoderDecoderInterface; -use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface as JsonEncoderNormalizerInterface; use Symfony\Component\JsonEncoder\EncoderInterface as JsonEncoderEncoderInterface; use Symfony\Component\JsonEncoder\JsonEncoder; +use Symfony\Component\JsonEncoder\ValueTransformer\ValueTransformerInterface; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\PersistingStoreInterface; @@ -2027,10 +2026,8 @@ private function registerJsonEncoderConfiguration(array $config, ContainerBuilde throw new LogicException('JsonEncoder support cannot be enabled as the JsonEncoder component is not installed. Try running "composer require symfony/json-encoder".'); } - $container->registerForAutoconfiguration(JsonEncoderNormalizerInterface::class) - ->addTag('json_encoder.normalizer'); - $container->registerForAutoconfiguration(JsonEncoderDenormalizerInterface::class) - ->addTag('json_encoder.denormalizer'); + $container->registerForAutoconfiguration(ValueTransformerInterface::class) + ->addTag('json_encoder.value_transformer'); $loader->load('json_encoder.php'); diff --git a/Resources/config/json_encoder.php b/Resources/config/json_encoder.php index 67cb25d0a..f6fbd05e6 100644 --- a/Resources/config/json_encoder.php +++ b/Resources/config/json_encoder.php @@ -13,8 +13,6 @@ use Symfony\Component\JsonEncoder\CacheWarmer\EncoderDecoderCacheWarmer; use Symfony\Component\JsonEncoder\CacheWarmer\LazyGhostCacheWarmer; -use Symfony\Component\JsonEncoder\Decode\Denormalizer\DateTimeDenormalizer; -use Symfony\Component\JsonEncoder\Encode\Normalizer\DateTimeNormalizer; use Symfony\Component\JsonEncoder\JsonDecoder; use Symfony\Component\JsonEncoder\JsonEncoder; use Symfony\Component\JsonEncoder\Mapping\Decode\AttributePropertyMetadataLoader as DecodeAttributePropertyMetadataLoader; @@ -23,19 +21,21 @@ use Symfony\Component\JsonEncoder\Mapping\Encode\DateTimeTypePropertyMetadataLoader as EncodeDateTimeTypePropertyMetadataLoader; use Symfony\Component\JsonEncoder\Mapping\GenericTypePropertyMetadataLoader; use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoader; +use Symfony\Component\JsonEncoder\ValueTransformer\DateTimeToStringValueTransformer; +use Symfony\Component\JsonEncoder\ValueTransformer\StringToDateTimeValueTransformer; return static function (ContainerConfigurator $container) { $container->services() // encoder/decoder ->set('json_encoder.encoder', JsonEncoder::class) ->args([ - tagged_locator('json_encoder.normalizer'), + tagged_locator('json_encoder.value_transformer'), service('json_encoder.encode.property_metadata_loader'), param('.json_encoder.encoders_dir'), ]) ->set('json_encoder.decoder', JsonDecoder::class) ->args([ - tagged_locator('json_encoder.denormalizer'), + tagged_locator('json_encoder.value_transformer'), service('json_encoder.decode.property_metadata_loader'), param('.json_encoder.decoders_dir'), param('.json_encoder.lazy_ghosts_dir'), @@ -63,7 +63,7 @@ ->decorate('json_encoder.encode.property_metadata_loader') ->args([ service('.inner'), - tagged_locator('json_encoder.normalizer'), + tagged_locator('json_encoder.value_transformer'), service('type_info.resolver'), ]) @@ -86,23 +86,16 @@ ->decorate('json_encoder.decode.property_metadata_loader') ->args([ service('.inner'), - tagged_locator('json_encoder.normalizer'), + tagged_locator('json_encoder.value_transformer'), service('type_info.resolver'), ]) - // normalizers/denormalizers - ->set('json_encoder.normalizer.date_time', DateTimeNormalizer::class) - ->tag('json_encoder.normalizer') - ->set('json_encoder.denormalizer.date_time', DateTimeDenormalizer::class) - ->args([ - false, - ]) - ->tag('json_encoder.denormalizer') - ->set('json_encoder.denormalizer.date_time_immutable', DateTimeDenormalizer::class) - ->args([ - true, - ]) - ->tag('json_encoder.denormalizer') + // value transformers + ->set('json_encoder.value_transformer.date_time_to_string', DateTimeToStringValueTransformer::class) + ->tag('json_encoder.value_transformer') + + ->set('json_encoder.value_transformer.string_to_date_time', StringToDateTimeValueTransformer::class) + ->tag('json_encoder.value_transformer') // cache ->set('.json_encoder.cache_warmer.encoder_decoder', EncoderDecoderCacheWarmer::class) diff --git a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php index 8610de049..a6b4d8e91 100644 --- a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php +++ b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php @@ -11,11 +11,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto; -use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer; -use Symfony\Component\JsonEncoder\Attribute\Denormalizer; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeToStringValueTransformer; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\StringToRangeValueTransformer; use Symfony\Component\JsonEncoder\Attribute\EncodedName; use Symfony\Component\JsonEncoder\Attribute\JsonEncodable; -use Symfony\Component\JsonEncoder\Attribute\Normalizer; +use Symfony\Component\JsonEncoder\Attribute\ValueTransformer; /** * @author Mathias Arlaud @@ -24,11 +24,9 @@ class Dummy { #[EncodedName('@name')] - #[Normalizer('strtoupper')] - #[Denormalizer('strtolower')] + #[ValueTransformer(toJsonValue: 'strtoupper', toNativeValue: 'strtolower')] public string $name = 'dummy'; - #[Normalizer(RangeNormalizer::class)] - #[Denormalizer(RangeNormalizer::class)] + #[ValueTransformer(toJsonValue: RangeToStringValueTransformer::class, toNativeValue: StringToRangeValueTransformer::class)] public array $range = [10, 20]; } diff --git a/Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php b/Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php new file mode 100644 index 000000000..93be1fad9 --- /dev/null +++ b/Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder; + +use Symfony\Component\JsonEncoder\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; + +/** + * @author Mathias Arlaud + */ +class RangeToStringValueTransformer implements ValueTransformerInterface +{ + public function transform(mixed $value, array $options = []): string + { + return $value[0].'..'.$value[1]; + } + + public static function getJsonValueType(): BuiltinType + { + return Type::string(); + } +} diff --git a/Tests/Functional/app/JsonEncoder/RangeNormalizer.php b/Tests/Functional/app/JsonEncoder/StringToRangeValueTransformer.php similarity index 51% rename from Tests/Functional/app/JsonEncoder/RangeNormalizer.php rename to Tests/Functional/app/JsonEncoder/StringToRangeValueTransformer.php index beb9e8188..46e3dd305 100644 --- a/Tests/Functional/app/JsonEncoder/RangeNormalizer.php +++ b/Tests/Functional/app/JsonEncoder/StringToRangeValueTransformer.php @@ -11,27 +11,21 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder; -use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface; -use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface; +use Symfony\Component\JsonEncoder\ValueTransformer\ValueTransformerInterface; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; /** * @author Mathias Arlaud */ -class RangeNormalizer implements NormalizerInterface, DenormalizerInterface +class StringToRangeValueTransformer implements ValueTransformerInterface { - public function normalize(mixed $denormalized, array $options = []): string + public function transform(mixed $value, array $options = []): array { - return $denormalized[0].'..'.$denormalized[1]; + return array_map(static fn (string $v): int => (int) $v, explode('..', $value)); } - public function denormalize(mixed $normalized, array $options = []): array - { - return array_map(static fn (string $v): int => (int) $v, explode('..', $normalized)); - } - - public static function getNormalizedType(): BuiltinType + public static function getJsonValueType(): BuiltinType { return Type::string(); } diff --git a/Tests/Functional/app/JsonEncoder/config.yml b/Tests/Functional/app/JsonEncoder/config.yml index 13b68adef..5eb5d4996 100644 --- a/Tests/Functional/app/JsonEncoder/config.yml +++ b/Tests/Functional/app/JsonEncoder/config.yml @@ -23,4 +23,5 @@ services: public: true Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy: ~ - Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeNormalizer: ~ + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\StringToRangeValueTransformer: ~ + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeToStringValueTransformer: ~ From 81758d2301826192269b9b46ecc9236ffcbf2075 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Thu, 20 Feb 2025 09:13:24 +0100 Subject: [PATCH 044/111] [Framework] Deprecate the `framework.validation.cache` config option --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55eeaf1fd..3b9873591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Add `RateLimiterFactoryInterface` as an alias of the `limiter` service * Add `framework.validation.disable_translation` option * Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration + * Deprecate the `framework.validation.cache` option 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index f5279c419..ca20f15a4 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1034,7 +1034,9 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->info('Validation configuration') ->{$enableIfStandalone('symfony/validator', Validation::class)}() ->children() - ->scalarNode('cache')->end() + ->scalarNode('cache') + ->setDeprecated('symfony/framework-bundle', '7.3', 'Setting the "%path%.%node%" configuration option is deprecated. It will be removed in version 8.0.') + ->end() ->booleanNode('enable_attributes')->{class_exists(FullStack::class) ? 'defaultFalse' : 'defaultTrue'}()->end() ->arrayNode('static_method') ->defaultValue(['loadValidatorMetadata']) From 73f2dab16365ad5ad224c4f39d2386b7c0d47fe6 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Fri, 21 Feb 2025 17:40:20 +0100 Subject: [PATCH 045/111] Refactor S/MIME encrypter to use certificate repository Replaces direct certificate path usage with a repository interface for managing S/MIME certificates. This improves flexibility by allowing custom certificate retrieval logic through `SmimeCertificateRepositoryInterface`. Adjusted related tests, configuration, and event listener implementation accordingly. --- DependencyInjection/Configuration.php | 4 ++-- DependencyInjection/FrameworkExtension.php | 6 ++---- Resources/config/mailer.php | 10 ++-------- Resources/config/schema/symfony-1.0.xsd | 2 +- Tests/DependencyInjection/ConfigurationTest.php | 2 +- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index ca20f15a4..7507ae392 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2314,8 +2314,8 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->canBeEnabled() ->info('S/MIME encrypter configuration') ->children() - ->scalarNode('certificate') - ->info('Path to certificate (in PEM format without the `file://` prefix)') + ->scalarNode('repository') + ->info('Path to the S/MIME certificate repository. Shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`.') ->defaultValue('') ->cannotBeEmpty() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 4c36bf4dc..331005d63 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2893,11 +2893,9 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co if (!class_exists(SmimeEncryptedMessageListener::class)) { throw new LogicException('S/MIME encrypted messages support cannot be enabled as this version of the Mailer component does not support it.'); } - $smimeDecrypter = $container->getDefinition('mailer.smime_encrypter'); - $smimeDecrypter->setArgument(0, $config['smime_encrypter']['certificate']); - $smimeDecrypter->setArgument(1, $config['smime_encrypter']['cipher']); + $container->setAlias('mailer.smime_encrypter.repository', $config['smime_encrypter']['repository']); + $container->setParameter('mailer.smime_encrypter.cipher', $config['smime_encrypter']['cipher']); } else { - $container->removeDefinition('mailer.smime_encrypter'); $container->removeDefinition('mailer.smime_encrypter.listener'); } diff --git a/Resources/config/mailer.php b/Resources/config/mailer.php index 25b3fefdb..71a43b9c8 100644 --- a/Resources/config/mailer.php +++ b/Resources/config/mailer.php @@ -26,7 +26,6 @@ use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mailer\Transport\Transports; use Symfony\Component\Mime\Crypto\DkimSigner; -use Symfony\Component\Mime\Crypto\SMimeEncrypter; use Symfony\Component\Mime\Crypto\SMimeSigner; return static function (ContainerConfigurator $container) { @@ -102,12 +101,6 @@ abstract_arg('signOptions'), ]) - ->set('mailer.smime_encrypter', SMimeEncrypter::class) - ->args([ - abstract_arg('certificate'), - abstract_arg('cipher'), - ]) - ->set('mailer.dkim_signer.listener', DkimSignedMessageListener::class) ->args([ service(DkimSigner::class), @@ -122,7 +115,8 @@ ->set('mailer.smime_encrypter.listener', SmimeEncryptedMessageListener::class) ->args([ - service('mailer.smime_encrypter'), + service('mailer.smime_encrypter.repository'), + param('mailer.smime_encrypter.cipher'), ]) ->tag('kernel.event_subscriber') diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index b47de331a..d961ca97c 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -836,7 +836,7 @@ - + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 607049274..c4b273dc8 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -941,7 +941,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor ], 'smime_encrypter' => [ 'enabled' => false, - 'certificate' => '', + 'repository' => '', 'cipher' => null, ], ], From 322f4966e013f27368fa375ba95e6f70a8b3abf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?El=C3=ADas=20Fern=C3=A1ndez?= Date: Tue, 25 Feb 2025 17:20:33 +0100 Subject: [PATCH 046/111] bug #59854 Fix mailer signer configuration issues --- DependencyInjection/FrameworkExtension.php | 4 ++-- Resources/config/mailer.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 4c36bf4dc..4d43f2089 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2879,8 +2879,8 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co throw new LogicException('SMIME signed messages support cannot be enabled as this version of the Mailer component does not support it.'); } $smimeSigner = $container->getDefinition('mailer.smime_signer'); - $smimeSigner->setArgument(0, $config['smime_signer']['key']); - $smimeSigner->setArgument(1, $config['smime_signer']['certificate']); + $smimeSigner->setArgument(0, $config['smime_signer']['certificate']); + $smimeSigner->setArgument(1, $config['smime_signer']['key']); $smimeSigner->setArgument(2, $config['smime_signer']['passphrase']); $smimeSigner->setArgument(3, $config['smime_signer']['extra_certificates']); $smimeSigner->setArgument(4, $config['smime_signer']['sign_options']); diff --git a/Resources/config/mailer.php b/Resources/config/mailer.php index 25b3fefdb..e7d2f98fa 100644 --- a/Resources/config/mailer.php +++ b/Resources/config/mailer.php @@ -95,8 +95,8 @@ ->set('mailer.smime_signer', SMimeSigner::class) ->args([ - abstract_arg('key'), abstract_arg('certificate'), + abstract_arg('key'), abstract_arg('passphrase'), abstract_arg('extraCertificates'), abstract_arg('signOptions'), @@ -110,7 +110,7 @@ ->set('mailer.dkim_signer.listener', DkimSignedMessageListener::class) ->args([ - service(DkimSigner::class), + service('mailer.dkim_signer'), ]) ->tag('kernel.event_subscriber') From 70bc1b3ed44ebb96271befbd2ab13e212e637367 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 25 Feb 2025 14:53:52 +0100 Subject: [PATCH 047/111] [JsonStreamer] Rename the component --- CHANGELOG.md | 2 +- .../Compiler/UnusedTagsPass.php | 2 +- DependencyInjection/Configuration.php | 12 +- DependencyInjection/FrameworkExtension.php | 42 +++---- FrameworkBundle.php | 4 +- Resources/config/json_encoder.php | 119 ------------------ Resources/config/json_streamer.php | 119 ++++++++++++++++++ Resources/config/schema/symfony-1.0.xsd | 4 +- .../DependencyInjection/ConfigurationTest.php | 6 +- .../{json_encoder.php => json_streamer.php} | 2 +- .../DependencyInjection/Fixtures/xml/full.xml | 2 +- .../{json_encoder.xml => json_streamer.xml} | 2 +- .../DependencyInjection/Fixtures/yml/full.yml | 2 +- .../{json_encoder.yml => json_streamer.yml} | 2 +- .../FrameworkExtensionTestCase.php | 6 +- Tests/Functional/JsonEncoderTest.php | 67 ---------- Tests/Functional/JsonStreamerTest.php | 67 ++++++++++ .../Functional/app/JsonEncoder/Dto/Dummy.php | 32 ----- Tests/Functional/app/JsonEncoder/config.yml | 27 ---- .../Functional/app/JsonStreamer/Dto/Dummy.php | 38 ++++++ .../RangeToStringValueTransformer.php | 6 +- .../StringToRangeValueTransformer.php | 6 +- .../{JsonEncoder => JsonStreamer}/bundles.php | 0 Tests/Functional/app/JsonStreamer/config.yml | 27 ++++ composer.json | 4 +- 25 files changed, 303 insertions(+), 297 deletions(-) delete mode 100644 Resources/config/json_encoder.php create mode 100644 Resources/config/json_streamer.php rename Tests/DependencyInjection/Fixtures/php/{json_encoder.php => json_streamer.php} (91%) rename Tests/DependencyInjection/Fixtures/xml/{json_encoder.xml => json_streamer.xml} (93%) rename Tests/DependencyInjection/Fixtures/yml/{json_encoder.yml => json_streamer.yml} (90%) delete mode 100644 Tests/Functional/JsonEncoderTest.php create mode 100644 Tests/Functional/JsonStreamerTest.php delete mode 100644 Tests/Functional/app/JsonEncoder/Dto/Dummy.php delete mode 100644 Tests/Functional/app/JsonEncoder/config.yml create mode 100644 Tests/Functional/app/JsonStreamer/Dto/Dummy.php rename Tests/Functional/app/{JsonEncoder => JsonStreamer}/RangeToStringValueTransformer.php (82%) rename Tests/Functional/app/{JsonEncoder => JsonStreamer}/StringToRangeValueTransformer.php (83%) rename Tests/Functional/app/{JsonEncoder => JsonStreamer}/bundles.php (100%) create mode 100644 Tests/Functional/app/JsonStreamer/config.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9873591..17201aeae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ CHANGELOG * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` - * Add JsonEncoder services and configuration + * Add JsonStreamer services and configuration * Add new `framework.property_info.with_constructor_extractor` option to allow enabling or disabling the constructor extractor integration * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown * Add `RateLimiterFactoryInterface` as an alias of the `limiter` service diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index c5191a659..c8271d463 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -53,7 +53,7 @@ class UnusedTagsPass implements CompilerPassInterface 'form.type_guesser', 'html_sanitizer', 'http_client.client', - 'json_encoder.value_transformer', + 'json_streamer.value_transformer', 'kernel.cache_clearer', 'kernel.cache_warmer', 'kernel.event_listener', diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index d4bf35257..ce77b50f8 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -31,7 +31,7 @@ use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\IpUtils; -use Symfony\Component\JsonEncoder\EncoderInterface; +use Symfony\Component\JsonStreamer\StreamWriterInterface; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; @@ -182,7 +182,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addHtmlSanitizerSection($rootNode, $enableIfStandalone); $this->addWebhookSection($rootNode, $enableIfStandalone); $this->addRemoteEventSection($rootNode, $enableIfStandalone); - $this->addJsonEncoderSection($rootNode, $enableIfStandalone); + $this->addJsonStreamerSection($rootNode, $enableIfStandalone); return $treeBuilder; } @@ -2692,13 +2692,13 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable ; } - private function addJsonEncoderSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void + private function addJsonStreamerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void { $rootNode ->children() - ->arrayNode('json_encoder') - ->info('JSON encoder configuration') - ->{$enableIfStandalone('symfony/json-encoder', EncoderInterface::class)}() + ->arrayNode('json_streamer') + ->info('JSON streamer configuration') + ->{$enableIfStandalone('symfony/json-streamer', StreamWriterInterface::class)}() ->end() ->end() ; diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 73024eaf8..6be0e2be7 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -100,11 +100,11 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; -use Symfony\Component\JsonEncoder\Attribute\JsonEncodable; -use Symfony\Component\JsonEncoder\DecoderInterface as JsonEncoderDecoderInterface; -use Symfony\Component\JsonEncoder\EncoderInterface as JsonEncoderEncoderInterface; -use Symfony\Component\JsonEncoder\JsonEncoder; -use Symfony\Component\JsonEncoder\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; +use Symfony\Component\JsonStreamer\JsonStreamWriter; +use Symfony\Component\JsonStreamer\StreamReaderInterface; +use Symfony\Component\JsonStreamer\StreamWriterInterface; +use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\PersistingStoreInterface; @@ -436,12 +436,12 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader); } - if ($this->readConfigEnabled('json_encoder', $container, $config['json_encoder'])) { + if ($this->readConfigEnabled('json_streamer', $container, $config['json_streamer'])) { if (!$typeInfoEnabled) { - throw new LogicException('JsonEncoder support cannot be enabled as the TypeInfo component is not '.(interface_exists(TypeResolverInterface::class) ? 'enabled.' : 'installed. Try running "composer require symfony/type-info".')); + throw new LogicException('JsonStreamer support cannot be enabled as the TypeInfo component is not '.(interface_exists(TypeResolverInterface::class) ? 'enabled.' : 'installed. Try running "composer require symfony/type-info".')); } - $this->registerJsonEncoderConfiguration($config['json_encoder'], $container, $loader); + $this->registerJsonStreamerConfiguration($config['json_streamer'], $container, $loader); } if ($this->readConfigEnabled('lock', $container, $config['lock'])) { @@ -755,8 +755,8 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } ); } - $container->registerAttributeForAutoconfiguration(JsonEncodable::class, static function (ChildDefinition $definition, JsonEncodable $attribute): void { - $definition->addTag('json_encoder.encodable', [ + $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute): void { + $definition->addTag('json_streamer.streamable', [ 'object' => $attribute->asObject, 'list' => $attribute->asList, ]); @@ -2033,26 +2033,26 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []); } - private function registerJsonEncoderConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void + private function registerJsonStreamerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void { - if (!class_exists(JsonEncoder::class)) { - throw new LogicException('JsonEncoder support cannot be enabled as the JsonEncoder component is not installed. Try running "composer require symfony/json-encoder".'); + if (!class_exists(JsonStreamWriter::class)) { + throw new LogicException('JsonStreamer support cannot be enabled as the JsonStreamer component is not installed. Try running "composer require symfony/json-streamer".'); } $container->registerForAutoconfiguration(ValueTransformerInterface::class) - ->addTag('json_encoder.value_transformer'); + ->addTag('json_streamer.value_transformer'); - $loader->load('json_encoder.php'); + $loader->load('json_streamer.php'); - $container->registerAliasForArgument('json_encoder.encoder', JsonEncoderEncoderInterface::class, 'json.encoder'); - $container->registerAliasForArgument('json_encoder.decoder', JsonEncoderDecoderInterface::class, 'json.decoder'); + $container->registerAliasForArgument('json_streamer.stream_writer', StreamWriterInterface::class, 'json.stream_writer'); + $container->registerAliasForArgument('json_streamer.stream_reader', StreamReaderInterface::class, 'json.stream_reader'); - $container->setParameter('.json_encoder.encoders_dir', '%kernel.cache_dir%/json_encoder/encoder'); - $container->setParameter('.json_encoder.decoders_dir', '%kernel.cache_dir%/json_encoder/decoder'); - $container->setParameter('.json_encoder.lazy_ghosts_dir', '%kernel.cache_dir%/json_encoder/lazy_ghost'); + $container->setParameter('.json_streamer.stream_writers_dir', '%kernel.cache_dir%/json_streamer/stream_writer'); + $container->setParameter('.json_streamer.stream_readers_dir', '%kernel.cache_dir%/json_streamer/stream_reader'); + $container->setParameter('.json_streamer.lazy_ghosts_dir', '%kernel.cache_dir%/json_streamer/lazy_ghost'); if (\PHP_VERSION_ID >= 80400) { - $container->removeDefinition('.json_encoder.cache_warmer.lazy_ghost'); + $container->removeDefinition('.json_streamer.cache_warmer.lazy_ghost'); } } diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 89d9744f5..faf2841f4 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -54,7 +54,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\JsonEncoder\DependencyInjection\EncodablePass; +use Symfony\Component\JsonStreamer\DependencyInjection\StreamablePass; use Symfony\Component\Messenger\DependencyInjection\MessengerPass; use Symfony\Component\Mime\DependencyInjection\AddMimeTypeGuesserPass; use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoConstructorPass; @@ -189,7 +189,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new VirtualRequestStackPass()); $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING); - $this->addCompilerPassIfExists($container, EncodablePass::class); + $this->addCompilerPassIfExists($container, StreamablePass::class); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/Resources/config/json_encoder.php b/Resources/config/json_encoder.php deleted file mode 100644 index f6fbd05e6..000000000 --- a/Resources/config/json_encoder.php +++ /dev/null @@ -1,119 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Loader\Configurator; - -use Symfony\Component\JsonEncoder\CacheWarmer\EncoderDecoderCacheWarmer; -use Symfony\Component\JsonEncoder\CacheWarmer\LazyGhostCacheWarmer; -use Symfony\Component\JsonEncoder\JsonDecoder; -use Symfony\Component\JsonEncoder\JsonEncoder; -use Symfony\Component\JsonEncoder\Mapping\Decode\AttributePropertyMetadataLoader as DecodeAttributePropertyMetadataLoader; -use Symfony\Component\JsonEncoder\Mapping\Decode\DateTimeTypePropertyMetadataLoader as DecodeDateTimeTypePropertyMetadataLoader; -use Symfony\Component\JsonEncoder\Mapping\Encode\AttributePropertyMetadataLoader as EncodeAttributePropertyMetadataLoader; -use Symfony\Component\JsonEncoder\Mapping\Encode\DateTimeTypePropertyMetadataLoader as EncodeDateTimeTypePropertyMetadataLoader; -use Symfony\Component\JsonEncoder\Mapping\GenericTypePropertyMetadataLoader; -use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoader; -use Symfony\Component\JsonEncoder\ValueTransformer\DateTimeToStringValueTransformer; -use Symfony\Component\JsonEncoder\ValueTransformer\StringToDateTimeValueTransformer; - -return static function (ContainerConfigurator $container) { - $container->services() - // encoder/decoder - ->set('json_encoder.encoder', JsonEncoder::class) - ->args([ - tagged_locator('json_encoder.value_transformer'), - service('json_encoder.encode.property_metadata_loader'), - param('.json_encoder.encoders_dir'), - ]) - ->set('json_encoder.decoder', JsonDecoder::class) - ->args([ - tagged_locator('json_encoder.value_transformer'), - service('json_encoder.decode.property_metadata_loader'), - param('.json_encoder.decoders_dir'), - param('.json_encoder.lazy_ghosts_dir'), - ]) - ->alias(JsonEncoder::class, 'json_encoder.encoder') - ->alias(JsonDecoder::class, 'json_encoder.decoder') - - // metadata - ->set('json_encoder.encode.property_metadata_loader', PropertyMetadataLoader::class) - ->args([ - service('type_info.resolver'), - ]) - ->set('.json_encoder.encode.property_metadata_loader.generic', GenericTypePropertyMetadataLoader::class) - ->decorate('json_encoder.encode.property_metadata_loader') - ->args([ - service('.inner'), - service('type_info.type_context_factory'), - ]) - ->set('.json_encoder.encode.property_metadata_loader.date_time', EncodeDateTimeTypePropertyMetadataLoader::class) - ->decorate('json_encoder.encode.property_metadata_loader') - ->args([ - service('.inner'), - ]) - ->set('.json_encoder.encode.property_metadata_loader.attribute', EncodeAttributePropertyMetadataLoader::class) - ->decorate('json_encoder.encode.property_metadata_loader') - ->args([ - service('.inner'), - tagged_locator('json_encoder.value_transformer'), - service('type_info.resolver'), - ]) - - ->set('json_encoder.decode.property_metadata_loader', PropertyMetadataLoader::class) - ->args([ - service('type_info.resolver'), - ]) - ->set('.json_encoder.decode.property_metadata_loader.generic', GenericTypePropertyMetadataLoader::class) - ->decorate('json_encoder.decode.property_metadata_loader') - ->args([ - service('.inner'), - service('type_info.type_context_factory'), - ]) - ->set('.json_encoder.decode.property_metadata_loader.date_time', DecodeDateTimeTypePropertyMetadataLoader::class) - ->decorate('json_encoder.decode.property_metadata_loader') - ->args([ - service('.inner'), - ]) - ->set('.json_encoder.decode.property_metadata_loader.attribute', DecodeAttributePropertyMetadataLoader::class) - ->decorate('json_encoder.decode.property_metadata_loader') - ->args([ - service('.inner'), - tagged_locator('json_encoder.value_transformer'), - service('type_info.resolver'), - ]) - - // value transformers - ->set('json_encoder.value_transformer.date_time_to_string', DateTimeToStringValueTransformer::class) - ->tag('json_encoder.value_transformer') - - ->set('json_encoder.value_transformer.string_to_date_time', StringToDateTimeValueTransformer::class) - ->tag('json_encoder.value_transformer') - - // cache - ->set('.json_encoder.cache_warmer.encoder_decoder', EncoderDecoderCacheWarmer::class) - ->args([ - abstract_arg('encodable class names'), - service('json_encoder.encode.property_metadata_loader'), - service('json_encoder.decode.property_metadata_loader'), - param('.json_encoder.encoders_dir'), - param('.json_encoder.decoders_dir'), - service('logger')->ignoreOnInvalid(), - ]) - ->tag('kernel.cache_warmer') - - ->set('.json_encoder.cache_warmer.lazy_ghost', LazyGhostCacheWarmer::class) - ->args([ - abstract_arg('encodable class names'), - param('.json_encoder.lazy_ghosts_dir'), - ]) - ->tag('kernel.cache_warmer') - ; -}; diff --git a/Resources/config/json_streamer.php b/Resources/config/json_streamer.php new file mode 100644 index 000000000..79fb25833 --- /dev/null +++ b/Resources/config/json_streamer.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\JsonStreamer\CacheWarmer\LazyGhostCacheWarmer; +use Symfony\Component\JsonStreamer\CacheWarmer\StreamerCacheWarmer; +use Symfony\Component\JsonStreamer\JsonStreamReader; +use Symfony\Component\JsonStreamer\JsonStreamWriter; +use Symfony\Component\JsonStreamer\Mapping\GenericTypePropertyMetadataLoader; +use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader; +use Symfony\Component\JsonStreamer\Mapping\Read\AttributePropertyMetadataLoader as ReadAttributePropertyMetadataLoader; +use Symfony\Component\JsonStreamer\Mapping\Read\DateTimeTypePropertyMetadataLoader as ReadDateTimeTypePropertyMetadataLoader; +use Symfony\Component\JsonStreamer\Mapping\Write\AttributePropertyMetadataLoader as WriteAttributePropertyMetadataLoader; +use Symfony\Component\JsonStreamer\Mapping\Write\DateTimeTypePropertyMetadataLoader as WriteDateTimeTypePropertyMetadataLoader; +use Symfony\Component\JsonStreamer\ValueTransformer\DateTimeToStringValueTransformer; +use Symfony\Component\JsonStreamer\ValueTransformer\StringToDateTimeValueTransformer; + +return static function (ContainerConfigurator $container) { + $container->services() + // stream reader/writer + ->set('json_streamer.stream_writer', JsonStreamWriter::class) + ->args([ + tagged_locator('json_streamer.value_transformer'), + service('json_streamer.write.property_metadata_loader'), + param('.json_streamer.stream_writers_dir'), + ]) + ->set('json_streamer.stream_reader', JsonStreamReader::class) + ->args([ + tagged_locator('json_streamer.value_transformer'), + service('json_streamer.read.property_metadata_loader'), + param('.json_streamer.stream_readers_dir'), + param('.json_streamer.lazy_ghosts_dir'), + ]) + ->alias(JsonStreamWriter::class, 'json_streamer.stream_writer') + ->alias(JsonStreamReader::class, 'json_streamer.stream_reader') + + // metadata + ->set('json_streamer.write.property_metadata_loader', PropertyMetadataLoader::class) + ->args([ + service('type_info.resolver'), + ]) + ->set('.json_streamer.write.property_metadata_loader.generic', GenericTypePropertyMetadataLoader::class) + ->decorate('json_streamer.write.property_metadata_loader') + ->args([ + service('.inner'), + service('type_info.type_context_factory'), + ]) + ->set('.json_streamer.write.property_metadata_loader.date_time', WriteDateTimeTypePropertyMetadataLoader::class) + ->decorate('json_streamer.write.property_metadata_loader') + ->args([ + service('.inner'), + ]) + ->set('.json_streamer.write.property_metadata_loader.attribute', WriteAttributePropertyMetadataLoader::class) + ->decorate('json_streamer.write.property_metadata_loader') + ->args([ + service('.inner'), + tagged_locator('json_streamer.value_transformer'), + service('type_info.resolver'), + ]) + + ->set('json_streamer.read.property_metadata_loader', PropertyMetadataLoader::class) + ->args([ + service('type_info.resolver'), + ]) + ->set('.json_streamer.read.property_metadata_loader.generic', GenericTypePropertyMetadataLoader::class) + ->decorate('json_streamer.read.property_metadata_loader') + ->args([ + service('.inner'), + service('type_info.type_context_factory'), + ]) + ->set('.json_streamer.read.property_metadata_loader.date_time', ReadDateTimeTypePropertyMetadataLoader::class) + ->decorate('json_streamer.read.property_metadata_loader') + ->args([ + service('.inner'), + ]) + ->set('.json_streamer.read.property_metadata_loader.attribute', ReadAttributePropertyMetadataLoader::class) + ->decorate('json_streamer.read.property_metadata_loader') + ->args([ + service('.inner'), + tagged_locator('json_streamer.value_transformer'), + service('type_info.resolver'), + ]) + + // value transformers + ->set('json_streamer.value_transformer.date_time_to_string', DateTimeToStringValueTransformer::class) + ->tag('json_streamer.value_transformer') + + ->set('json_streamer.value_transformer.string_to_date_time', StringToDateTimeValueTransformer::class) + ->tag('json_streamer.value_transformer') + + // cache + ->set('.json_streamer.cache_warmer.streamer', StreamerCacheWarmer::class) + ->args([ + abstract_arg('streamable'), + service('json_streamer.write.property_metadata_loader'), + service('json_streamer.read.property_metadata_loader'), + param('.json_streamer.stream_writers_dir'), + param('.json_streamer.stream_readers_dir'), + service('logger')->ignoreOnInvalid(), + ]) + ->tag('kernel.cache_warmer') + + ->set('.json_streamer.cache_warmer.lazy_ghost', LazyGhostCacheWarmer::class) + ->args([ + abstract_arg('streamable class names'), + param('.json_streamer.lazy_ghosts_dir'), + ]) + ->tag('kernel.cache_warmer') + ; +}; diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index b47de331a..ae69a5aa9 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -46,7 +46,7 @@ - + @@ -1041,7 +1041,7 @@ - + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 6dbb0f0e3..6842cf9e9 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HtmlSanitizer\HtmlSanitizer; use Symfony\Component\HttpClient\HttpClient; -use Symfony\Component\JsonEncoder\JsonEncoder; +use Symfony\Component\JsonStreamer\JsonStreamWriter; use Symfony\Component\Lock\Store\SemaphoreStore; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\MessageBusInterface; @@ -1009,8 +1009,8 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'remote-event' => [ 'enabled' => !class_exists(FullStack::class) && class_exists(RemoteEvent::class), ], - 'json_encoder' => [ - 'enabled' => !class_exists(FullStack::class) && class_exists(JsonEncoder::class), + 'json_streamer' => [ + 'enabled' => !class_exists(FullStack::class) && class_exists(JsonStreamWriter::class), ], ]; } diff --git a/Tests/DependencyInjection/Fixtures/php/json_encoder.php b/Tests/DependencyInjection/Fixtures/php/json_streamer.php similarity index 91% rename from Tests/DependencyInjection/Fixtures/php/json_encoder.php rename to Tests/DependencyInjection/Fixtures/php/json_streamer.php index 42204b2cb..844b72004 100644 --- a/Tests/DependencyInjection/Fixtures/php/json_encoder.php +++ b/Tests/DependencyInjection/Fixtures/php/json_streamer.php @@ -8,7 +8,7 @@ 'type_info' => [ 'enabled' => true, ], - 'json_encoder' => [ + 'json_streamer' => [ 'enabled' => true, ], ]); diff --git a/Tests/DependencyInjection/Fixtures/xml/full.xml b/Tests/DependencyInjection/Fixtures/xml/full.xml index 23d325e61..07faf22ab 100644 --- a/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -46,6 +46,6 @@ - + diff --git a/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml b/Tests/DependencyInjection/Fixtures/xml/json_streamer.xml similarity index 93% rename from Tests/DependencyInjection/Fixtures/xml/json_encoder.xml rename to Tests/DependencyInjection/Fixtures/xml/json_streamer.xml index a20f98567..5c79cb840 100644 --- a/Tests/DependencyInjection/Fixtures/xml/json_encoder.xml +++ b/Tests/DependencyInjection/Fixtures/xml/json_streamer.xml @@ -9,6 +9,6 @@ - + diff --git a/Tests/DependencyInjection/Fixtures/yml/full.yml b/Tests/DependencyInjection/Fixtures/yml/full.yml index 28c4336d9..8a1a3834b 100644 --- a/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -71,4 +71,4 @@ framework: formats: csv: ['text/csv', 'text/plain'] pdf: 'application/pdf' - json_encoder: ~ + json_streamer: ~ diff --git a/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml b/Tests/DependencyInjection/Fixtures/yml/json_streamer.yml similarity index 90% rename from Tests/DependencyInjection/Fixtures/yml/json_encoder.yml rename to Tests/DependencyInjection/Fixtures/yml/json_streamer.yml index e09f7c7d3..8873fea97 100644 --- a/Tests/DependencyInjection/Fixtures/yml/json_encoder.yml +++ b/Tests/DependencyInjection/Fixtures/yml/json_streamer.yml @@ -6,5 +6,5 @@ framework: log: true type_info: enabled: true - json_encoder: + json_streamer: enabled: true diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index d48dd6ad6..8581e4c8b 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -2551,10 +2551,10 @@ public function testSemaphoreWithService() self::assertEquals(new Reference('my_service'), $storeDef->getArgument(0)); } - public function testJsonEncoderEnabled() + public function testJsonStreamerEnabled() { - $container = $this->createContainerFromFile('json_encoder'); - $this->assertTrue($container->has('json_encoder.encoder')); + $container = $this->createContainerFromFile('json_streamer'); + $this->assertTrue($container->has('json_streamer.stream_writer')); } protected function createContainer(array $data = []) diff --git a/Tests/Functional/JsonEncoderTest.php b/Tests/Functional/JsonEncoderTest.php deleted file mode 100644 index b5410e1e1..000000000 --- a/Tests/Functional/JsonEncoderTest.php +++ /dev/null @@ -1,67 +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\Functional; - -use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy; -use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\JsonEncoder\DecoderInterface; -use Symfony\Component\JsonEncoder\EncoderInterface; -use Symfony\Component\TypeInfo\Type; - -/** - * @author Mathias Arlaud - */ -class JsonEncoderTest extends AbstractWebTestCase -{ - protected function setUp(): void - { - static::bootKernel(['test_case' => 'JsonEncoder']); - } - - public function testEncode() - { - /** @var EncoderInterface $encoder */ - $encoder = static::getContainer()->get('json_encoder.encoder.alias'); - - $this->assertSame('{"@name":"DUMMY","range":"10..20"}', (string) $encoder->encode(new Dummy(), Type::object(Dummy::class))); - } - - public function testDecode() - { - /** @var DecoderInterface $decoder */ - $decoder = static::getContainer()->get('json_encoder.decoder.alias'); - - $expected = new Dummy(); - $expected->name = 'dummy'; - $expected->range = [0, 1]; - - $this->assertEquals($expected, $decoder->decode('{"@name": "DUMMY", "range": "0..1"}', Type::object(Dummy::class))); - } - - public function testWarmupEncodableClasses() - { - /** @var Filesystem $fs */ - $fs = static::getContainer()->get('filesystem'); - - $encodersDir = \sprintf('%s/json_encoder/encoder/', static::getContainer()->getParameter('kernel.cache_dir')); - - // clear already created encoders - if ($fs->exists($encodersDir)) { - $fs->remove($encodersDir); - } - - static::getContainer()->get('json_encoder.cache_warmer.encoder_decoder.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir')); - - $this->assertFileExists($encodersDir); - $this->assertCount(2, glob($encodersDir.'/*')); - } -} diff --git a/Tests/Functional/JsonStreamerTest.php b/Tests/Functional/JsonStreamerTest.php new file mode 100644 index 000000000..9816015b4 --- /dev/null +++ b/Tests/Functional/JsonStreamerTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\Dto\Dummy; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\JsonStreamer\StreamReaderInterface; +use Symfony\Component\JsonStreamer\StreamWriterInterface; +use Symfony\Component\TypeInfo\Type; + +/** + * @author Mathias Arlaud + */ +class JsonStreamerTest extends AbstractWebTestCase +{ + protected function setUp(): void + { + static::bootKernel(['test_case' => 'JsonStreamer']); + } + + public function testWrite() + { + /** @var StreamWriterInterface $writer */ + $writer = static::getContainer()->get('json_streamer.stream_writer.alias'); + + $this->assertSame('{"@name":"DUMMY","range":"10..20"}', (string) $writer->write(new Dummy(), Type::object(Dummy::class))); + } + + public function testRead() + { + /** @var StreamReaderInterface $reader */ + $reader = static::getContainer()->get('json_streamer.stream_reader.alias'); + + $expected = new Dummy(); + $expected->name = 'dummy'; + $expected->range = [0, 1]; + + $this->assertEquals($expected, $reader->read('{"@name": "DUMMY", "range": "0..1"}', Type::object(Dummy::class))); + } + + public function testWarmupStreamableClasses() + { + /** @var Filesystem $fs */ + $fs = static::getContainer()->get('filesystem'); + + $streamWritersDir = \sprintf('%s/json_streamer/stream_writer/', static::getContainer()->getParameter('kernel.cache_dir')); + + // clear already created stream writers + if ($fs->exists($streamWritersDir)) { + $fs->remove($streamWritersDir); + } + + static::getContainer()->get('json_streamer.cache_warmer.streamer.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir')); + + $this->assertFileExists($streamWritersDir); + $this->assertCount(2, glob($streamWritersDir.'/*')); + } +} diff --git a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php b/Tests/Functional/app/JsonEncoder/Dto/Dummy.php deleted file mode 100644 index a6b4d8e91..000000000 --- a/Tests/Functional/app/JsonEncoder/Dto/Dummy.php +++ /dev/null @@ -1,32 +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\Functional\app\JsonEncoder\Dto; - -use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeToStringValueTransformer; -use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\StringToRangeValueTransformer; -use Symfony\Component\JsonEncoder\Attribute\EncodedName; -use Symfony\Component\JsonEncoder\Attribute\JsonEncodable; -use Symfony\Component\JsonEncoder\Attribute\ValueTransformer; - -/** - * @author Mathias Arlaud - */ -#[JsonEncodable] -class Dummy -{ - #[EncodedName('@name')] - #[ValueTransformer(toJsonValue: 'strtoupper', toNativeValue: 'strtolower')] - public string $name = 'dummy'; - - #[ValueTransformer(toJsonValue: RangeToStringValueTransformer::class, toNativeValue: StringToRangeValueTransformer::class)] - public array $range = [10, 20]; -} diff --git a/Tests/Functional/app/JsonEncoder/config.yml b/Tests/Functional/app/JsonEncoder/config.yml deleted file mode 100644 index 5eb5d4996..000000000 --- a/Tests/Functional/app/JsonEncoder/config.yml +++ /dev/null @@ -1,27 +0,0 @@ -imports: - - { resource: ../config/default.yml } - -framework: - http_method_override: false - type_info: ~ - json_encoder: ~ - -services: - _defaults: - autoconfigure: true - - json_encoder.encoder.alias: - alias: json_encoder.encoder - public: true - - json_encoder.decoder.alias: - alias: json_encoder.decoder - public: true - - json_encoder.cache_warmer.encoder_decoder.alias: - alias: .json_encoder.cache_warmer.encoder_decoder - public: true - - Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\Dto\Dummy: ~ - Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\StringToRangeValueTransformer: ~ - Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder\RangeToStringValueTransformer: ~ diff --git a/Tests/Functional/app/JsonStreamer/Dto/Dummy.php b/Tests/Functional/app/JsonStreamer/Dto/Dummy.php new file mode 100644 index 000000000..d1f1ca67a --- /dev/null +++ b/Tests/Functional/app/JsonStreamer/Dto/Dummy.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\Dto; + +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\RangeToStringValueTransformer; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\StringToRangeValueTransformer; +use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; +use Symfony\Component\JsonStreamer\Attribute\StreamedName; +use Symfony\Component\JsonStreamer\Attribute\ValueTransformer; + +/** + * @author Mathias Arlaud + */ +#[JsonStreamable] +class Dummy +{ + #[StreamedName('@name')] + #[ValueTransformer( + nativeToStream: 'strtoupper', + streamToNative: 'strtolower', + )] + public string $name = 'dummy'; + + #[ValueTransformer( + nativeToStream: RangeToStringValueTransformer::class, + streamToNative: StringToRangeValueTransformer::class, + )] + public array $range = [10, 20]; +} diff --git a/Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php b/Tests/Functional/app/JsonStreamer/RangeToStringValueTransformer.php similarity index 82% rename from Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php rename to Tests/Functional/app/JsonStreamer/RangeToStringValueTransformer.php index 93be1fad9..6d21f2d2f 100644 --- a/Tests/Functional/app/JsonEncoder/RangeToStringValueTransformer.php +++ b/Tests/Functional/app/JsonStreamer/RangeToStringValueTransformer.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder; +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer; -use Symfony\Component\JsonEncoder\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; @@ -25,7 +25,7 @@ public function transform(mixed $value, array $options = []): string return $value[0].'..'.$value[1]; } - public static function getJsonValueType(): BuiltinType + public static function getStreamValueType(): BuiltinType { return Type::string(); } diff --git a/Tests/Functional/app/JsonEncoder/StringToRangeValueTransformer.php b/Tests/Functional/app/JsonStreamer/StringToRangeValueTransformer.php similarity index 83% rename from Tests/Functional/app/JsonEncoder/StringToRangeValueTransformer.php rename to Tests/Functional/app/JsonStreamer/StringToRangeValueTransformer.php index 46e3dd305..398beb2ff 100644 --- a/Tests/Functional/app/JsonEncoder/StringToRangeValueTransformer.php +++ b/Tests/Functional/app/JsonStreamer/StringToRangeValueTransformer.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonEncoder; +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer; -use Symfony\Component\JsonEncoder\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; @@ -25,7 +25,7 @@ public function transform(mixed $value, array $options = []): array return array_map(static fn (string $v): int => (int) $v, explode('..', $value)); } - public static function getJsonValueType(): BuiltinType + public static function getStreamValueType(): BuiltinType { return Type::string(); } diff --git a/Tests/Functional/app/JsonEncoder/bundles.php b/Tests/Functional/app/JsonStreamer/bundles.php similarity index 100% rename from Tests/Functional/app/JsonEncoder/bundles.php rename to Tests/Functional/app/JsonStreamer/bundles.php diff --git a/Tests/Functional/app/JsonStreamer/config.yml b/Tests/Functional/app/JsonStreamer/config.yml new file mode 100644 index 000000000..188869b82 --- /dev/null +++ b/Tests/Functional/app/JsonStreamer/config.yml @@ -0,0 +1,27 @@ +imports: + - { resource: ../config/default.yml } + +framework: + http_method_override: false + type_info: ~ + json_streamer: ~ + +services: + _defaults: + autoconfigure: true + + json_streamer.stream_writer.alias: + alias: json_streamer.stream_writer + public: true + + json_streamer.stream_reader.alias: + alias: json_streamer.stream_reader + public: true + + json_streamer.cache_warmer.streamer.alias: + alias: .json_streamer.cache_warmer.streamer + public: true + + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\Dto\Dummy: ~ + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\StringToRangeValueTransformer: ~ + Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\RangeToStringValueTransformer: ~ diff --git a/composer.json b/composer.json index 03707eea3..a39899d26 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "symfony/workflow": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0", "symfony/property-info": "^6.4|^7.0", - "symfony/json-encoder": "7.3.*", + "symfony/json-streamer": "7.3.*", "symfony/uid": "^6.4|^7.0", "symfony/web-link": "^6.4|^7.0", "symfony/webhook": "^7.2", @@ -88,7 +88,7 @@ "symfony/dom-crawler": "<6.4", "symfony/http-client": "<6.4", "symfony/form": "<6.4", - "symfony/json-encoder": ">=7.4", + "symfony/json-streamer": ">=7.4", "symfony/lock": "<6.4", "symfony/mailer": "<6.4", "symfony/messenger": "<6.4", From 4a870f8f5fd405e3a7c1460add640b4a21e928cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 28 Feb 2025 17:51:43 +0100 Subject: [PATCH 048/111] Fix CS --- Test/HttpClientAssertionsTrait.php | 3 +-- Test/NotificationAssertionsTrait.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Test/HttpClientAssertionsTrait.php b/Test/HttpClientAssertionsTrait.php index 20c64608e..01a27ea87 100644 --- a/Test/HttpClientAssertionsTrait.php +++ b/Test/HttpClientAssertionsTrait.php @@ -14,10 +14,9 @@ use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector; -/* +/** * @author Mathieu Santostefano */ - trait HttpClientAssertionsTrait { public static function assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client'): void diff --git a/Test/NotificationAssertionsTrait.php b/Test/NotificationAssertionsTrait.php index b68473561..2c4c5467d 100644 --- a/Test/NotificationAssertionsTrait.php +++ b/Test/NotificationAssertionsTrait.php @@ -17,7 +17,7 @@ use Symfony\Component\Notifier\Message\MessageInterface; use Symfony\Component\Notifier\Test\Constraint as NotifierConstraint; -/* +/** * @author Smaïne Milianni */ trait NotificationAssertionsTrait From 018191a292a0ce7006b7bb26e02ee6cb07d2d313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFck=20Piera?= Date: Tue, 5 Nov 2024 15:14:54 +0100 Subject: [PATCH 049/111] [ErrorHandler] Add a command to dump static error pages --- Resources/config/console.php | 10 ++++++++++ composer.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Resources/config/console.php b/Resources/config/console.php index 7168caa4d..7ef10bb52 100644 --- a/Resources/config/console.php +++ b/Resources/config/console.php @@ -44,6 +44,7 @@ use Symfony\Component\Console\EventListener\ErrorListener; use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand; +use Symfony\Component\ErrorHandler\Command\ErrorDumpCommand; use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; use Symfony\Component\Messenger\Command\DebugCommand as MessengerDebugCommand; use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; @@ -59,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\WebpackEncoreBundle\Asset\EntrypointLookupInterface; return static function (ContainerConfigurator $container) { $container->services() @@ -385,6 +387,14 @@ ]) ->tag('console.command') + ->set('console.command.error_dumper', ErrorDumpCommand::class) + ->args([ + service('filesystem'), + service('error_renderer.html'), + service(EntrypointLookupInterface::class)->nullOnInvalid(), + ]) + ->tag('console.command') + ->set('console.messenger.application', Application::class) ->share(false) ->call('setAutoExit', [false]) diff --git a/composer.json b/composer.json index 03707eea3..ec7977291 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "symfony/config": "^7.3", "symfony/dependency-injection": "^7.2", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", + "symfony/error-handler": "^7.3", "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^7.3", "symfony/http-kernel": "^7.2", From a4b66fbb797c2d55525f933d9f54ddb2a6578c7e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 26 Feb 2025 18:04:06 +0100 Subject: [PATCH 050/111] Add support for `valkey:` / `valkeys:` schemes --- DependencyInjection/Configuration.php | 1 + DependencyInjection/FrameworkExtension.php | 7 ++++--- Resources/config/cache.php | 2 ++ Tests/DependencyInjection/ConfigurationTest.php | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index ce77b50f8..fd77de26e 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1285,6 +1285,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBe ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/pools/app')->end() ->scalarNode('default_psr6_provider')->end() ->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end() + ->scalarNode('default_valkey_provider')->defaultValue('valkey://localhost')->end() ->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() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 45f4366c1..418ee739c 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2501,7 +2501,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con // Inline any env vars referenced in the parameter $container->setParameter('cache.prefix.seed', $container->resolveEnvPlaceholders($container->getParameter('cache.prefix.seed'), true)); } - foreach (['psr6', 'redis', 'memcached', 'doctrine_dbal', 'pdo'] as $name) { + foreach (['psr6', 'redis', 'valkey', 'memcached', 'doctrine_dbal', 'pdo'] as $name) { if (isset($config[$name = 'default_'.$name.'_provider'])) { $container->setAlias('cache.'.$name, new Alias(CachePoolPass::getServiceProvider($container, $config[$name]), false)); } @@ -2513,12 +2513,13 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con 'tags' => false, ]; } + $redisTagAwareAdapters = [['cache.adapter.redis_tag_aware'], ['cache.adapter.valkey_tag_aware']]; foreach ($config['pools'] as $name => $pool) { $pool['adapters'] = $pool['adapters'] ?: ['cache.app']; - $isRedisTagAware = ['cache.adapter.redis_tag_aware'] === $pool['adapters']; + $isRedisTagAware = \in_array($pool['adapters'], $redisTagAwareAdapters, true); foreach ($pool['adapters'] as $provider => $adapter) { - if (($config['pools'][$adapter]['adapters'] ?? null) === ['cache.adapter.redis_tag_aware']) { + if (\in_array($config['pools'][$adapter]['adapters'] ?? null, $redisTagAwareAdapters, true)) { $isRedisTagAware = true; } elseif ($config['pools'][$adapter]['tags'] ?? false) { $pool['adapters'][$provider] = $adapter = '.'.$adapter.'.inner'; diff --git a/Resources/config/cache.php b/Resources/config/cache.php index ad4dca42d..3d96ba059 100644 --- a/Resources/config/cache.php +++ b/Resources/config/cache.php @@ -140,6 +140,7 @@ 'reset' => 'reset', ]) ->tag('monolog.logger', ['channel' => 'cache']) + ->alias('cache.adapter.valkey', 'cache.adapter.redis') ->set('cache.adapter.redis_tag_aware', RedisTagAwareAdapter::class) ->abstract() @@ -156,6 +157,7 @@ 'reset' => 'reset', ]) ->tag('monolog.logger', ['channel' => 'cache']) + ->alias('cache.adapter.valkey_tag_aware', 'cache.adapter.redis_tag_aware') ->set('cache.adapter.memcached', MemcachedAdapter::class) ->abstract() diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 6842cf9e9..e4ec77451 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -883,6 +883,7 @@ protected static function getBundleDefaultConfig() 'system' => 'cache.adapter.system', 'directory' => '%kernel.cache_dir%/pools/app', 'default_redis_provider' => 'redis://localhost', + 'default_valkey_provider' => 'valkey://localhost', 'default_memcached_provider' => 'memcached://localhost', 'default_doctrine_dbal_provider' => 'database_connection', 'default_pdo_provider' => ContainerBuilder::willBeAvailable('doctrine/dbal', Connection::class, ['symfony/framework-bundle']) && class_exists(DoctrineAdapter::class) ? 'database_connection' : null, From 5121dfd2f5504ef2a639c01e0dc73975c2d46091 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 2 Mar 2025 16:03:52 +0100 Subject: [PATCH 051/111] replace assertEmpty() with stricter assertions --- Tests/DependencyInjection/FrameworkExtensionTestCase.php | 4 ++-- Tests/Functional/ApiAttributesTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 8581e4c8b..3a719b863 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -796,7 +796,7 @@ public function testMessengerServicesRemovedWhenDisabled() \ARRAY_FILTER_USE_KEY ); - $this->assertEmpty($messengerDefinitions); + $this->assertSame([], $messengerDefinitions); $this->assertFalse($container->hasDefinition('console.command.messenger_consume_messages')); $this->assertFalse($container->hasDefinition('console.command.messenger_debug')); $this->assertFalse($container->hasDefinition('console.command.messenger_stop_workers')); @@ -1941,7 +1941,7 @@ public function testRemovesResourceCheckerConfigCacheFactoryArgumentOnlyIfNoDebu $container = $this->createContainer(['kernel.debug' => false]); (new FrameworkExtension())->load([['annotations' => false, 'http_method_override' => false, 'handle_all_throwables' => true, 'php_errors' => ['log' => true]]], $container); - $this->assertEmpty($container->getDefinition('config_cache_factory')->getArguments()); + $this->assertSame([], $container->getDefinition('config_cache_factory')->getArguments()); } public function testLoggerAwareRegistration() diff --git a/Tests/Functional/ApiAttributesTest.php b/Tests/Functional/ApiAttributesTest.php index cf5c384ba..0dcfeaeba 100644 --- a/Tests/Functional/ApiAttributesTest.php +++ b/Tests/Functional/ApiAttributesTest.php @@ -34,7 +34,7 @@ public function testMapQueryString(string $uri, array $query, string $expectedRe if ($expectedResponse) { self::assertJsonStringEqualsJsonString($expectedResponse, $response->getContent()); } else { - self::assertEmpty($response->getContent()); + self::assertSame('', $response->getContent()); } self::assertSame($expectedStatusCode, $response->getStatusCode()); } From 52b51ab8ac5a9f41893b678d94bd83ab11c0e65e Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Thu, 6 Mar 2025 11:54:56 -0500 Subject: [PATCH 052/111] [FrameworkBundle] fix autowiring `RateLimiterFactoryInterface` --- CHANGELOG.md | 2 +- DependencyInjection/FrameworkExtension.php | 5 +++++ Resources/config/rate_limiter.php | 6 ------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17201aeae..3e10d305f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ CHANGELOG * Add JsonStreamer services and configuration * Add new `framework.property_info.with_constructor_extractor` option to allow enabling or disabling the constructor extractor integration * Deprecate the `--show-arguments` option of the `container:debug` command, as arguments are now always shown - * Add `RateLimiterFactoryInterface` as an alias of the `limiter` service + * Add autowiring alias for `RateLimiterFactoryInterface` * Add `framework.validation.disable_translation` option * Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration * Deprecate the `framework.validation.cache` option diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 418ee739c..49078e2ee 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -151,6 +151,7 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\RateLimiter\LimiterInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; +use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; use Symfony\Component\RateLimiter\Storage\CacheStorage; use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; use Symfony\Component\RemoteEvent\RemoteEvent; @@ -3201,6 +3202,10 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde $limiter->replaceArgument(0, $limiterConfig); $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); + + if (interface_exists(RateLimiterFactoryInterface::class)) { + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); + } } } diff --git a/Resources/config/rate_limiter.php b/Resources/config/rate_limiter.php index 90af4d758..727a1f636 100644 --- a/Resources/config/rate_limiter.php +++ b/Resources/config/rate_limiter.php @@ -12,7 +12,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\RateLimiter\RateLimiterFactory; -use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; return static function (ContainerConfigurator $container) { $container->services() @@ -28,9 +27,4 @@ null, ]) ; - - if (interface_exists(RateLimiterFactoryInterface::class)) { - $container->services() - ->alias(RateLimiterFactoryInterface::class, 'limiter'); - } }; From 373bdf534646e196b60851bc362023a89fd78159 Mon Sep 17 00:00:00 2001 From: Florian Cellier Date: Mon, 18 Nov 2024 12:13:09 +0100 Subject: [PATCH 053/111] [AssetMapper] Add `--dry-run` option on `importmap:require` command --- Resources/config/asset_mapper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/config/asset_mapper.php b/Resources/config/asset_mapper.php index c18755864..eeb1ceb4f 100644 --- a/Resources/config/asset_mapper.php +++ b/Resources/config/asset_mapper.php @@ -232,6 +232,7 @@ ->args([ service('asset_mapper.importmap.manager'), service('asset_mapper.importmap.version_checker'), + param('kernel.project_dir'), ]) ->tag('console.command') From ce42a91b77510a7fd2eebafba989299783b7ee05 Mon Sep 17 00:00:00 2001 From: "hubert.lenoir" Date: Fri, 29 Dec 2023 10:09:04 +0100 Subject: [PATCH 054/111] [Translation] Allow default parameters --- DependencyInjection/Configuration.php | 28 +++++++++++++++++ DependencyInjection/FrameworkExtension.php | 5 +++ Resources/config/schema/symfony-1.0.xsd | 19 ++++++++++++ .../DependencyInjection/ConfigurationTest.php | 1 + .../Fixtures/php/translator_globals.php | 15 +++++++++ .../php/translator_without_globals.php | 9 ++++++ .../Fixtures/xml/translator_globals.xml | 20 ++++++++++++ .../xml/translator_without_globals.xml | 14 +++++++++ .../Fixtures/yml/translator_globals.yml | 11 +++++++ .../yml/translator_without_globals.yml | 8 +++++ .../FrameworkExtensionTestCase.php | 31 +++++++++++++++++++ composer.json | 4 +-- 12 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 Tests/DependencyInjection/Fixtures/php/translator_globals.php create mode 100644 Tests/DependencyInjection/Fixtures/php/translator_without_globals.php create mode 100644 Tests/DependencyInjection/Fixtures/xml/translator_globals.xml create mode 100644 Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml create mode 100644 Tests/DependencyInjection/Fixtures/yml/translator_globals.yml create mode 100644 Tests/DependencyInjection/Fixtures/yml/translator_without_globals.yml diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index fd77de26e..17b4a438b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -967,6 +967,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->fixXmlConfig('fallback') ->fixXmlConfig('path') ->fixXmlConfig('provider') + ->fixXmlConfig('global') ->children() ->arrayNode('fallbacks') ->info('Defaults to the value of "default_locale".') @@ -1021,6 +1022,33 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->defaultValue([]) ->end() + ->arrayNode('globals') + ->info('Global parameters.') + ->example(['app_version' => 3.14]) + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->arrayPrototype() + ->fixXmlConfig('parameter') + ->children() + ->variableNode('value')->end() + ->stringNode('message')->end() + ->arrayNode('parameters') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->scalarPrototype()->end() + ->end() + ->stringNode('domain')->end() + ->end() + ->beforeNormalization() + ->ifTrue(static fn ($v) => !\is_array($v)) + ->then(static fn ($v) => ['value' => $v]) + ->end() + ->validate() + ->ifTrue(static fn ($v) => !(isset($v['value']) xor isset($v['message']))) + ->thenInvalid('The "globals" parameter should be either a string or an array with a "value" or a "message" key') + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 418ee739c..fdcae2a34 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -185,6 +185,7 @@ use Symfony\Component\Translation\Extractor\PhpAstExtractor; use Symfony\Component\Translation\LocaleSwitcher; use Symfony\Component\Translation\PseudoLocalizationTranslator; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Translation\Translator; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeResolver\PhpDocAwareReflectionTypeResolver; @@ -1614,6 +1615,10 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $translator->replaceArgument(4, $options); } + foreach ($config['globals'] as $name => $global) { + $translator->addMethodCall('addGlobalParameter', [$name, $global['value'] ?? new Definition(TranslatableMessage::class, [$global['message'], $global['parameters'] ?? [], $global['domain'] ?? null])]); + } + if ($config['pseudo_localization']['enabled']) { $options = $config['pseudo_localization']; unset($options['enabled']); diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index ae69a5aa9..8661384e2 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -256,6 +256,7 @@ + @@ -285,6 +286,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index e4ec77451..babfb2eab 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -785,6 +785,7 @@ protected static function getBundleDefaultConfig() 'localizable_html_attributes' => [], ], 'providers' => [], + 'globals' => [], ], 'validation' => [ 'enabled' => !class_exists(FullStack::class), diff --git a/Tests/DependencyInjection/Fixtures/php/translator_globals.php b/Tests/DependencyInjection/Fixtures/php/translator_globals.php new file mode 100644 index 000000000..8ee438ff9 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/translator_globals.php @@ -0,0 +1,15 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'translator' => [ + 'globals' => [ + '%%app_name%%' => 'My application', + '{app_version}' => '1.2.3', + '{url}' => ['message' => 'url', 'parameters' => ['scheme' => 'https://'], 'domain' => 'global'], + ], + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/php/translator_without_globals.php b/Tests/DependencyInjection/Fixtures/php/translator_without_globals.php new file mode 100644 index 000000000..fcc65c968 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/translator_without_globals.php @@ -0,0 +1,9 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'translator' => ['globals' => []], +]); diff --git a/Tests/DependencyInjection/Fixtures/xml/translator_globals.xml b/Tests/DependencyInjection/Fixtures/xml/translator_globals.xml new file mode 100644 index 000000000..017fd9393 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/translator_globals.xml @@ -0,0 +1,20 @@ + + + + + + + + + My application + + + https:// + + + + diff --git a/Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml b/Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml new file mode 100644 index 000000000..6c686bd30 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/yml/translator_globals.yml b/Tests/DependencyInjection/Fixtures/yml/translator_globals.yml new file mode 100644 index 000000000..ed42b676c --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/translator_globals.yml @@ -0,0 +1,11 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + translator: + globals: + '%%app_name%%': 'My application' + '{app_version}': '1.2.3' + '{url}': { message: 'url', parameters: { scheme: 'https://' }, domain: 'global' } diff --git a/Tests/DependencyInjection/Fixtures/yml/translator_without_globals.yml b/Tests/DependencyInjection/Fixtures/yml/translator_without_globals.yml new file mode 100644 index 000000000..dc7323868 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/translator_without_globals.yml @@ -0,0 +1,8 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + translator: + globals: [] diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 3a719b863..fdc586cc9 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -83,6 +83,7 @@ use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Translation\LocaleSwitcher; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -1241,6 +1242,36 @@ public function testTranslatorCacheDirDisabled() $this->assertNull($options['cache_dir']); } + public function testTranslatorGlobals() + { + $container = $this->createContainerFromFile('translator_globals'); + + $calls = $container->getDefinition('translator.default')->getMethodCalls(); + + $this->assertCount(5, $calls); + $this->assertSame( + ['addGlobalParameter', ['%%app_name%%', 'My application']], + $calls[2], + ); + $this->assertSame( + ['addGlobalParameter', ['{app_version}', '1.2.3']], + $calls[3], + ); + $this->assertEquals( + ['addGlobalParameter', ['{url}', new Definition(TranslatableMessage::class, ['url', ['scheme' => 'https://'], 'global'])]], + $calls[4], + ); + } + + public function testTranslatorWithoutGlobals() + { + $container = $this->createContainerFromFile('translator_without_globals'); + + $calls = $container->getDefinition('translator.default')->getMethodCalls(); + + $this->assertCount(2, $calls); + } + public function testValidation() { $container = $this->createContainerFromFile('full'); diff --git a/composer.json b/composer.json index 999dd7fed..72d33ed88 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "symfony/serializer": "^7.1", "symfony/stopwatch": "^6.4|^7.0", "symfony/string": "^6.4|^7.0", - "symfony/translation": "^6.4.3|^7.0", + "symfony/translation": "^7.3", "symfony/twig-bundle": "^6.4|^7.0", "symfony/type-info": "^7.1", "symfony/validator": "^6.4|^7.0", @@ -101,7 +101,7 @@ "symfony/security-core": "<6.4", "symfony/serializer": "<7.1", "symfony/stopwatch": "<6.4", - "symfony/translation": "<6.4.3", + "symfony/translation": "<7.3", "symfony/twig-bridge": "<6.4", "symfony/twig-bundle": "<6.4", "symfony/validator": "<6.4", From cfe5c8beb6052fb0292023c05ade0dc866cc3c59 Mon Sep 17 00:00:00 2001 From: Santiago San Martin Date: Tue, 4 Mar 2025 15:18:16 -0300 Subject: [PATCH 055/111] Add 'method' option to debug:router command to filter routes by HTTP method --- CHANGELOG.md | 1 + Command/RouterDebugCommand.php | 10 +++-- Console/Descriptor/Descriptor.php | 18 ++++++++- .../Descriptor/AbstractDescriptorTestCase.php | 16 ++++++++ Tests/Console/Descriptor/ObjectsProvider.php | 32 ++++++++++++++++ .../Descriptor/route_collection_2.json | 38 +++++++++++++++++++ .../Fixtures/Descriptor/route_collection_2.md | 37 ++++++++++++++++++ .../Descriptor/route_collection_2.txt | 7 ++++ .../Descriptor/route_collection_2.xml | 33 ++++++++++++++++ .../Descriptor/route_collection_3.json | 19 ++++++++++ .../Fixtures/Descriptor/route_collection_3.md | 18 +++++++++ .../Descriptor/route_collection_3.txt | 6 +++ .../Descriptor/route_collection_3.xml | 17 +++++++++ 13 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 Tests/Fixtures/Descriptor/route_collection_2.json create mode 100644 Tests/Fixtures/Descriptor/route_collection_2.md create mode 100644 Tests/Fixtures/Descriptor/route_collection_2.txt create mode 100644 Tests/Fixtures/Descriptor/route_collection_2.xml create mode 100644 Tests/Fixtures/Descriptor/route_collection_3.json create mode 100644 Tests/Fixtures/Descriptor/route_collection_3.md create mode 100644 Tests/Fixtures/Descriptor/route_collection_3.txt create mode 100644 Tests/Fixtures/Descriptor/route_collection_3.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 17201aeae..8340a4970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG * Add `framework.validation.disable_translation` option * Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration * Deprecate the `framework.validation.cache` option + * Add `--method` option to the `debug:router` command 7.2 --- diff --git a/Command/RouterDebugCommand.php b/Command/RouterDebugCommand.php index 13a6f75d0..e54377115 100644 --- a/Command/RouterDebugCommand.php +++ b/Command/RouterDebugCommand.php @@ -55,6 +55,7 @@ protected function configure(): void new InputOption('show-aliases', null, InputOption::VALUE_NONE, 'Show aliases in overview'), new InputOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), + 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: @@ -76,6 +77,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('name'); + $method = strtoupper($input->getOption('method')); $helper = new DescriptorHelper($this->fileLinkFormatter); $routes = $this->router->getRouteCollection(); $container = null; @@ -85,7 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($name) { $route = $routes->get($name); - $matchingRoutes = $this->findRouteNameContaining($name, $routes); + $matchingRoutes = $this->findRouteNameContaining($name, $routes, $method); if (!$input->isInteractive() && !$route && \count($matchingRoutes) > 1) { $helper->describe($io, $this->findRouteContaining($name, $routes), [ @@ -94,6 +96,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'show_controllers' => $input->getOption('show-controllers'), 'show_aliases' => $input->getOption('show-aliases'), 'output' => $io, + 'method' => $method, ]); return 0; @@ -124,17 +127,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'show_aliases' => $input->getOption('show-aliases'), 'output' => $io, 'container' => $container, + 'method' => $method, ]); } return 0; } - private function findRouteNameContaining(string $name, RouteCollection $routes): array + private function findRouteNameContaining(string $name, RouteCollection $routes, string $method): array { $foundRoutesNames = []; foreach ($routes as $routeName => $route) { - if (false !== stripos($routeName, $name)) { + if (false !== stripos($routeName, $name) && (!$method || !$route->getMethods() || \in_array($method, $route->getMethods(), true))) { $foundRoutesNames[] = $routeName; } } diff --git a/Console/Descriptor/Descriptor.php b/Console/Descriptor/Descriptor.php index af5c3b10a..e76b74247 100644 --- a/Console/Descriptor/Descriptor.php +++ b/Console/Descriptor/Descriptor.php @@ -49,7 +49,7 @@ public function describe(OutputInterface $output, mixed $object, array $options } match (true) { - $object instanceof RouteCollection => $this->describeRouteCollection($object, $options), + $object instanceof RouteCollection => $this->describeRouteCollection($this->filterRoutesByHttpMethod($object, $options['method'] ?? ''), $options), $object instanceof Route => $this->describeRoute($object, $options), $object instanceof ParameterBag => $this->describeContainerParameters($object, $options), $object instanceof ContainerBuilder && !empty($options['env-vars']) => $this->describeContainerEnvVars($this->getContainerEnvVars($object), $options), @@ -360,4 +360,20 @@ protected function getServiceEdges(ContainerBuilder $container, string $serviceI return []; } } + + private function filterRoutesByHttpMethod(RouteCollection $routes, string $method): RouteCollection + { + if (!$method) { + return $routes; + } + $filteredRoutes = clone $routes; + + foreach ($filteredRoutes as $routeName => $route) { + if ($route->getMethods() && !\in_array($method, $route->getMethods(), true)) { + $filteredRoutes->remove($routeName); + } + } + + return $filteredRoutes; + } } diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php index 477bd1014..eb18fbcc7 100644 --- a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php @@ -50,6 +50,21 @@ public static function getDescribeRouteCollectionTestData(): array return static::getDescriptionTestData(ObjectsProvider::getRouteCollections()); } + /** @dataProvider getDescribeRouteCollectionWithHttpMethodFilterTestData */ + public function testDescribeRouteCollectionWithHttpMethodFilter(string $httpMethod, RouteCollection $routes, $expectedDescription) + { + $this->assertDescription($expectedDescription, $routes, ['method' => $httpMethod]); + } + + public static function getDescribeRouteCollectionWithHttpMethodFilterTestData(): iterable + { + foreach (ObjectsProvider::getRouteCollectionsByHttpMethod() as $httpMethod => $routeCollection) { + foreach (static::getDescriptionTestData($routeCollection) as $testData) { + yield [$httpMethod, ...$testData]; + } + } + } + /** @dataProvider getDescribeRouteTestData */ public function testDescribeRoute(Route $route, $expectedDescription) { @@ -273,6 +288,7 @@ private function assertDescription($expectedDescription, $describedObject, array $options['is_debug'] = false; $options['raw_output'] = true; $options['raw_text'] = true; + $options['method'] ??= null; $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); if ('txt' === $this->getFormat()) { diff --git a/Tests/Console/Descriptor/ObjectsProvider.php b/Tests/Console/Descriptor/ObjectsProvider.php index 84adc4ac9..8eb1c4386 100644 --- a/Tests/Console/Descriptor/ObjectsProvider.php +++ b/Tests/Console/Descriptor/ObjectsProvider.php @@ -37,6 +37,38 @@ public static function getRouteCollections() return ['route_collection_1' => $collection1]; } + public static function getRouteCollectionsByHttpMethod(): array + { + $collection = new RouteCollection(); + foreach (self::getRoutes() as $name => $route) { + $collection->add($name, $route); + } + + // Clone the original collection and add a route without any specific method restrictions + $collectionWithRouteWithoutMethodRestriction = clone $collection; + $collectionWithRouteWithoutMethodRestriction->add( + 'route_3', + new RouteStub( + '/other/route', + [], + [], + ['opt1' => 'val1', 'opt2' => 'val2'], + 'localhost', + ['http', 'https'], + [], + ) + ); + + return [ + 'GET' => [ + 'route_collection_2' => $collectionWithRouteWithoutMethodRestriction, + ], + 'PUT' => [ + 'route_collection_3' => $collection, + ], + ]; + } + public static function getRoutes() { return [ diff --git a/Tests/Fixtures/Descriptor/route_collection_2.json b/Tests/Fixtures/Descriptor/route_collection_2.json new file mode 100644 index 000000000..8f5d2c743 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_2.json @@ -0,0 +1,38 @@ +{ + "route_1": { + "path": "\/hello\/{name}", + "pathRegex": "#PATH_REGEX#", + "host": "localhost", + "hostRegex": "#HOST_REGEX#", + "scheme": "http|https", + "method": "GET|HEAD", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub", + "defaults": { + "name": "Joseph" + }, + "requirements": { + "name": "[a-z]+" + }, + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + } + }, + "route_3": { + "path": "\/other\/route", + "pathRegex": "#PATH_REGEX#", + "host": "localhost", + "hostRegex": "#HOST_REGEX#", + "scheme": "http|https", + "method": "ANY", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub", + "defaults": [], + "requirements": "NO CUSTOM", + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + } + } +} diff --git a/Tests/Fixtures/Descriptor/route_collection_2.md b/Tests/Fixtures/Descriptor/route_collection_2.md new file mode 100644 index 000000000..e1b11e4a4 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_2.md @@ -0,0 +1,37 @@ +route_1 +------- + +- Path: /hello/{name} +- Path Regex: #PATH_REGEX# +- Host: localhost +- Host Regex: #HOST_REGEX# +- Scheme: http|https +- Method: GET|HEAD +- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub +- Defaults: + - `name`: Joseph +- Requirements: + - `name`: [a-z]+ +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler + - `opt1`: val1 + - `opt2`: val2 + + +route_3 +------- + +- Path: /other/route +- Path Regex: #PATH_REGEX# +- Host: localhost +- Host Regex: #HOST_REGEX# +- Scheme: http|https +- Method: ANY +- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub +- Defaults: NONE +- Requirements: NO CUSTOM +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler + - `opt1`: val1 + - `opt2`: val2 + diff --git a/Tests/Fixtures/Descriptor/route_collection_2.txt b/Tests/Fixtures/Descriptor/route_collection_2.txt new file mode 100644 index 000000000..a9f9ee21b --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_2.txt @@ -0,0 +1,7 @@ + --------- ---------- ------------ ----------- --------------- +  Name   Method   Scheme   Host   Path  + --------- ---------- ------------ ----------- --------------- + 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_2.xml b/Tests/Fixtures/Descriptor/route_collection_2.xml new file mode 100644 index 000000000..18c41deb7 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_2.xml @@ -0,0 +1,33 @@ + + + + /hello/{name} + localhost + http + https + GET + HEAD + + Joseph + + + [a-z]+ + + + + + + + + + /other/route + localhost + http + https + + + + + + + diff --git a/Tests/Fixtures/Descriptor/route_collection_3.json b/Tests/Fixtures/Descriptor/route_collection_3.json new file mode 100644 index 000000000..cabc8e0a7 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_3.json @@ -0,0 +1,19 @@ +{ + "route_2": { + "path": "\/name\/add", + "pathRegex": "#PATH_REGEX#", + "host": "localhost", + "hostRegex": "#HOST_REGEX#", + "scheme": "http|https", + "method": "PUT|POST", + "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\RouteStub", + "defaults": [], + "requirements": "NO CUSTOM", + "options": { + "compiler_class": "Symfony\\Component\\Routing\\RouteCompiler", + "opt1": "val1", + "opt2": "val2" + }, + "condition": "context.getMethod() in ['GET', 'HEAD', 'POST']" + } +} diff --git a/Tests/Fixtures/Descriptor/route_collection_3.md b/Tests/Fixtures/Descriptor/route_collection_3.md new file mode 100644 index 000000000..20fdabb95 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_3.md @@ -0,0 +1,18 @@ +route_2 +------- + +- Path: /name/add +- Path Regex: #PATH_REGEX# +- Host: localhost +- Host Regex: #HOST_REGEX# +- Scheme: http|https +- Method: PUT|POST +- Class: Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub +- Defaults: NONE +- Requirements: NO CUSTOM +- Options: + - `compiler_class`: Symfony\Component\Routing\RouteCompiler + - `opt1`: val1 + - `opt2`: val2 +- Condition: context.getMethod() in ['GET', 'HEAD', 'POST'] + diff --git a/Tests/Fixtures/Descriptor/route_collection_3.txt b/Tests/Fixtures/Descriptor/route_collection_3.txt new file mode 100644 index 000000000..8822b3c40 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_3.txt @@ -0,0 +1,6 @@ + --------- ---------- ------------ ----------- ----------- +  Name   Method   Scheme   Host   Path  + --------- ---------- ------------ ----------- ----------- + route_2 PUT|POST http|https localhost /name/add + --------- ---------- ------------ ----------- ----------- + diff --git a/Tests/Fixtures/Descriptor/route_collection_3.xml b/Tests/Fixtures/Descriptor/route_collection_3.xml new file mode 100644 index 000000000..57a05d4c1 --- /dev/null +++ b/Tests/Fixtures/Descriptor/route_collection_3.xml @@ -0,0 +1,17 @@ + + + + /name/add + localhost + http + https + PUT + POST + + + + + + context.getMethod() in ['GET', 'HEAD', 'POST'] + + From 7ee3ca4ef02f16e33000ec8e3b1b5a2a68af8924 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 19 Feb 2025 15:04:36 +0100 Subject: [PATCH 056/111] [Cache] Enable namespace-based invalidation by prefixing keys with backend-native namespace separators --- DependencyInjection/FrameworkExtension.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index e2d0888db..353fd1942 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -208,6 +208,7 @@ use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CallbackInterface; +use Symfony\Contracts\Cache\NamespacedPoolInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -2576,6 +2577,10 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con $container->registerAliasForArgument($tagAwareId, TagAwareCacheInterface::class, $pool['name'] ?? $name); $container->registerAliasForArgument($name, CacheInterface::class, $pool['name'] ?? $name); $container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name'] ?? $name); + + if (interface_exists(NamespacedPoolInterface::class)) { + $container->registerAliasForArgument($name, NamespacedPoolInterface::class, $pool['name'] ?? $name); + } } $definition->setPublic($pool['public']); From 2da58e6d5bb3559bcacf9bf81aac5ef6f97d3b43 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Fri, 14 Mar 2025 19:20:21 -0400 Subject: [PATCH 057/111] Fixed support for Kernel as command --- Tests/Kernel/KernelCommand.php | 26 ++++++++++++++++++++++++++ Tests/Kernel/MicroKernelTraitTest.php | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 Tests/Kernel/KernelCommand.php diff --git a/Tests/Kernel/KernelCommand.php b/Tests/Kernel/KernelCommand.php new file mode 100644 index 000000000..4c9a5d85a --- /dev/null +++ b/Tests/Kernel/KernelCommand.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand(name: 'kernel:hello')] +final class KernelCommand extends MinimalKernel +{ + public function __invoke(OutputInterface $output): int + { + $output->write('Hello Kernel!'); + + return 0; + } +} diff --git a/Tests/Kernel/MicroKernelTraitTest.php b/Tests/Kernel/MicroKernelTraitTest.php index a9d2ae720..5c7161124 100644 --- a/Tests/Kernel/MicroKernelTraitTest.php +++ b/Tests/Kernel/MicroKernelTraitTest.php @@ -13,7 +13,11 @@ 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; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; @@ -152,6 +156,23 @@ public function testSimpleKernel() $this->assertSame('Hello World!', $response->getContent()); } + public function testKernelCommand() + { + if (!property_exists(AsCommand::class, 'help')) { + $this->markTestSkipped('Invokable command no available.'); + } + + $kernel = $this->kernel = new KernelCommand('kernel_command'); + $application = new Application($kernel); + + $input = new ArrayInput(['command' => 'kernel:hello']); + $output = new BufferedOutput(); + + $this->assertTrue($application->has('kernel:hello')); + $this->assertSame(0, $application->doRun($input, $output)); + $this->assertSame('Hello Kernel!', $output->fetch()); + } + public function testDefaultKernel() { $kernel = $this->kernel = new DefaultKernel('test', false); From 0c33fbb75e67682eea5cec638075bf87ac3ad3d4 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 29 Jan 2024 10:12:01 +0100 Subject: [PATCH 058/111] [FrameworkBundle] Remove redundant `name` attribute from `default_context` --- DependencyInjection/Configuration.php | 1 - 1 file changed, 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 1939337bb..cb52a0704 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1196,7 +1196,6 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->arrayNode('default_context') ->normalizeKeys(false) - ->useAttributeAsKey('name') ->validate() ->ifTrue(fn () => $this->debug && class_exists(JsonParser::class)) ->then(fn (array $v) => $v + [JsonDecode::DETAILED_ERROR_MESSAGES => true]) From b17181f38963cbc2e160a71a5b8c2c9971375574 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 16 Mar 2025 15:56:09 +0100 Subject: [PATCH 059/111] [FrameworkBundle] Auto-exclude DI extensions, test cases, entities and messenger messages --- CHANGELOG.md | 1 + DependencyInjection/FrameworkExtension.php | 29 +++++++++++++++++++--- Kernel/MicroKernelTrait.php | 3 +-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2337f5ae0..9d3b3eae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ CHANGELOG * Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration * Deprecate the `framework.validation.cache` option * Add `--method` option to the `debug:router` command + * Auto-exclude DI extensions, test cases, entities and messenger messages 7.2 --- diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index e2d0888db..9ee0f0f43 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -12,12 +12,16 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Composer\InstalledVersions; +use Doctrine\ORM\Mapping\Embeddable; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\MappedSuperclass; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Types\ContextFactory; use PhpParser\Parser; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; use Psr\Clock\ClockInterface as PsrClockInterface; use Psr\Container\ContainerInterface as PsrContainerInterface; @@ -57,6 +61,7 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -117,6 +122,7 @@ use Symfony\Component\Mailer\EventListener\SmimeSignedMessageListener; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mercure\HubRegistry; +use Symfony\Component\Messenger\Attribute\AsMessage; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Bridge as MessengerBridge; use Symfony\Component\Messenger\Handler\BatchHandlerInterface; @@ -757,12 +763,29 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } ); } - $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute): void { - $definition->addTag('json_streamer.streamable', [ + + $container->registerForAutoconfiguration(CompilerPassInterface::class) + ->addExcludeTag('container.excluded.compiler_pass'); + $container->registerForAutoconfiguration(TestCase::class) + ->addExcludeTag('container.excluded.test_case'); + $container->registerAttributeForAutoconfiguration(AsMessage::class, static function (ChildDefinition $definition) { + $definition->addExcludeTag('container.excluded.messenger.message'); + }); + $container->registerAttributeForAutoconfiguration(Entity::class, static function (ChildDefinition $definition) { + $definition->addExcludeTag('container.excluded.doctrine.entity'); + }); + $container->registerAttributeForAutoconfiguration(Embeddable::class, static function (ChildDefinition $definition) { + $definition->addExcludeTag('container.excluded.doctrine.embeddable'); + }); + $container->registerAttributeForAutoconfiguration(MappedSuperclass::class, static function (ChildDefinition $definition) { + $definition->addExcludeTag('container.excluded.doctrine.mapped_superclass'); + }); + + $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute) { + $definition->addExcludeTag('json_streamer.streamable', [ 'object' => $attribute->asObject, 'list' => $attribute->asList, ]); - $definition->addTag('container.excluded'); }); if (!$container->getParameter('kernel.debug')) { diff --git a/Kernel/MicroKernelTrait.php b/Kernel/MicroKernelTrait.php index f40373a30..28d616c13 100644 --- a/Kernel/MicroKernelTrait.php +++ b/Kernel/MicroKernelTrait.php @@ -165,6 +165,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void ->setPublic(true) ; } + $container->setAlias($kernelClass, 'kernel')->setPublic(true); $kernelDefinition = $container->getDefinition('kernel'); $kernelDefinition->addTag('routing.route_loader'); @@ -197,8 +198,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $kernelLoader->registerAliasesForSinglyImplementedInterfaces(); AbstractConfigurator::$valuePreProcessor = $valuePreProcessor; } - - $container->setAlias($kernelClass, 'kernel')->setPublic(true); }); } From 719e035947bd6657312400889c7352cda76eecd3 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 18 Mar 2025 20:37:55 -0400 Subject: [PATCH 060/111] [DI] Rename "exclude tag" to "resource tag" --- DependencyInjection/FrameworkExtension.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 9ee0f0f43..2860457a8 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -765,24 +765,24 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } $container->registerForAutoconfiguration(CompilerPassInterface::class) - ->addExcludeTag('container.excluded.compiler_pass'); + ->addResourceTag('container.excluded.compiler_pass'); $container->registerForAutoconfiguration(TestCase::class) - ->addExcludeTag('container.excluded.test_case'); + ->addResourceTag('container.excluded.test_case'); $container->registerAttributeForAutoconfiguration(AsMessage::class, static function (ChildDefinition $definition) { - $definition->addExcludeTag('container.excluded.messenger.message'); + $definition->addResourceTag('container.excluded.messenger.message'); }); $container->registerAttributeForAutoconfiguration(Entity::class, static function (ChildDefinition $definition) { - $definition->addExcludeTag('container.excluded.doctrine.entity'); + $definition->addResourceTag('container.excluded.doctrine.entity'); }); $container->registerAttributeForAutoconfiguration(Embeddable::class, static function (ChildDefinition $definition) { - $definition->addExcludeTag('container.excluded.doctrine.embeddable'); + $definition->addResourceTag('container.excluded.doctrine.embeddable'); }); $container->registerAttributeForAutoconfiguration(MappedSuperclass::class, static function (ChildDefinition $definition) { - $definition->addExcludeTag('container.excluded.doctrine.mapped_superclass'); + $definition->addResourceTag('container.excluded.doctrine.mapped_superclass'); }); $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute) { - $definition->addExcludeTag('json_streamer.streamable', [ + $definition->addResourceTag('json_streamer.streamable', [ 'object' => $attribute->asObject, 'list' => $attribute->asList, ]); From 914cb2cf66f6220cf29dd9244c4d7b81620a3b1d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 18 Mar 2025 10:01:33 +0100 Subject: [PATCH 061/111] fix compatibility with DependencyInjection < 7.3 --- DependencyInjection/FrameworkExtension.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 2860457a8..2f2df8535 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -765,27 +765,29 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } $container->registerForAutoconfiguration(CompilerPassInterface::class) - ->addResourceTag('container.excluded.compiler_pass'); + ->addTag('container.excluded.compiler_pass')->addTag('container.excluded')->setAbstract(true); $container->registerForAutoconfiguration(TestCase::class) - ->addResourceTag('container.excluded.test_case'); + ->addTag('container.excluded.test_case')->addTag('container.excluded')->setAbstract(true); $container->registerAttributeForAutoconfiguration(AsMessage::class, static function (ChildDefinition $definition) { - $definition->addResourceTag('container.excluded.messenger.message'); + $definition->addTag('container.excluded.messenger.message')->addTag('container.excluded')->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(Entity::class, static function (ChildDefinition $definition) { - $definition->addResourceTag('container.excluded.doctrine.entity'); + $definition->addTag('container.excluded.doctrine.entity')->addTag('container.excluded')->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(Embeddable::class, static function (ChildDefinition $definition) { - $definition->addResourceTag('container.excluded.doctrine.embeddable'); + $definition->addTag('container.excluded.doctrine.embeddable')->addTag('container.excluded')->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(MappedSuperclass::class, static function (ChildDefinition $definition) { - $definition->addResourceTag('container.excluded.doctrine.mapped_superclass'); + $definition->addTag('container.excluded.doctrine.mapped_superclass')->addTag('container.excluded')->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute) { - $definition->addResourceTag('json_streamer.streamable', [ + $definition->addTag('json_streamer.streamable', [ 'object' => $attribute->asObject, 'list' => $attribute->asList, ]); + $definition->addTag('container.excluded'); + $definition->setAbstract(true); }); if (!$container->getParameter('kernel.debug')) { From 11b2364b6cadbbd9f576c76653b447ec5321718d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 21 Mar 2025 13:14:10 +0100 Subject: [PATCH 062/111] [FrameworkBundle] Add alias `ServicesResetter` for `services_resetter` service --- CHANGELOG.md | 1 + Resources/config/services.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3b3eae4..5ffc9f85f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ CHANGELOG * Deprecate the `framework.validation.cache` option * Add `--method` option to the `debug:router` command * Auto-exclude DI extensions, test cases, entities and messenger messages + * Add DI alias from `ServicesResetterInterface` to `services_resetter` 7.2 --- diff --git a/Resources/config/services.php b/Resources/config/services.php index e5a86d8f4..558c2b6d5 100644 --- a/Resources/config/services.php +++ b/Resources/config/services.php @@ -42,6 +42,7 @@ use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate; use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; +use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetterInterface; use Symfony\Component\HttpKernel\EventListener\LocaleAwareListener; use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpCache\StoreInterface; @@ -177,6 +178,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->set('services_resetter', ServicesResetter::class) ->public() + ->alias(ServicesResetterInterface::class, 'services_resetter') ->set('reverse_container', ReverseContainer::class) ->args([ From 51418a20079cb25af3fcb8fa8ae1ed82f7fdd1ce Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sun, 23 Mar 2025 17:46:24 +0100 Subject: [PATCH 063/111] [Serializer] Fix code skipped by premature return --- DependencyInjection/FrameworkExtension.php | 24 ++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 1c5725335..f585b5bbb 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2004,24 +2004,22 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('serializer.default_context', $defaultContext); } - if (!$container->hasDefinition('serializer.normalizer.object')) { - return; - } + if ($container->hasDefinition('serializer.normalizer.object')) { + $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); + $context = $arguments[6] ?? $defaultContext; - $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = $arguments[6] ?? $defaultContext; + if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { + $context += ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; + $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); + } - if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { - $context += ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; - $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); - } + if ($config['max_depth_handler'] ?? false) { + $context += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; + } - if ($config['max_depth_handler'] ?? false) { - $context += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; + $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); } - $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); - $container->getDefinition('serializer.normalizer.property')->setArgument(5, $defaultContext); } From 68875215bf74a481a64d345d81510214845adf94 Mon Sep 17 00:00:00 2001 From: Oviglo Date: Thu, 20 Mar 2025 09:12:34 +0100 Subject: [PATCH 064/111] [Security] Add methods param in IsCsrfTokenValid attribute --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffc9f85f..9dd35a826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * Add `--method` option to the `debug:router` command * Auto-exclude DI extensions, test cases, entities and messenger messages * Add DI alias from `ServicesResetterInterface` to `services_resetter` + * Add `methods` argument in `#[IsCsrfTokenValid]` attribute 7.2 --- From 98c231361b54e3939a6a2fb4ac5a01f46c18f7c0 Mon Sep 17 00:00:00 2001 From: Arkalo2 <24898676+Arkalo2@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:57:41 +0100 Subject: [PATCH 065/111] [FrameworkBundle][HttpKernel] Allow configuring the logging channel per type of exceptions --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 4 ++++ DependencyInjection/FrameworkExtension.php | 15 ++++++++++++++- Resources/config/web.php | 1 + .../FrameworkExtensionTestCase.php | 4 ++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd35a826..63eca480b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ CHANGELOG * Auto-exclude DI extensions, test cases, entities and messenger messages * Add DI alias from `ServicesResetterInterface` to `services_resetter` * Add `methods` argument in `#[IsCsrfTokenValid]` attribute + * Allow configuring the logging channel per type of exceptions 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 17b4a438b..bb6a3347b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1458,6 +1458,10 @@ private function addExceptionsSection(ArrayNodeDefinition $rootNode): void ->end() ->defaultNull() ->end() + ->scalarNode('log_channel') + ->info('The channel of log message. Null to let Symfony decide.') + ->defaultNull() + ->end() ->end() ->end() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index b8b723d1d..fed06a6c7 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -417,7 +417,20 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); $this->registerSecretsConfiguration($config['secrets'], $container, $loader, $config['secret'] ?? null); - $container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']); + $exceptionListener = $container->getDefinition('exception_listener'); + + $loggers = []; + foreach ($config['exceptions'] as $exception) { + if (!isset($exception['log_channel'])) { + continue; + } + $loggers[$exception['log_channel']] = new Reference('monolog.logger.'.$exception['log_channel'], ContainerInterface::NULL_ON_INVALID_REFERENCE); + } + + $exceptionListener + ->replaceArgument(3, $config['exceptions']) + ->setArgument(4, $loggers) + ; if ($this->readConfigEnabled('serializer', $container, $config['serializer'])) { if (!class_exists(Serializer::class)) { diff --git a/Resources/config/web.php b/Resources/config/web.php index 6f8358fb0..a4e975dac 100644 --- a/Resources/config/web.php +++ b/Resources/config/web.php @@ -138,6 +138,7 @@ service('logger')->nullOnInvalid(), param('kernel.debug'), abstract_arg('an exceptions to log & status code mapping'), + abstract_arg('list of loggers by log_channel'), ]) ->tag('kernel.event_subscriber') ->tag('monolog.logger', ['channel' => 'request']) diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index fdc586cc9..9edbf111e 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -615,21 +615,25 @@ public function testExceptionsConfig() ], array_keys($configuration)); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => 'info', 'status_code' => 422, ], $configuration[\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class]); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => 'info', 'status_code' => null, ], $configuration[\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class]); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => 'info', 'status_code' => null, ], $configuration[\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class]); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => null, 'status_code' => 500, ], $configuration[\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class]); From c1c6ee8946491b698b067df2258e07918c25da02 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 24 Mar 2025 05:35:59 +0100 Subject: [PATCH 066/111] [Serializer] Fix ObjectNormalizer default context with named serializers --- DependencyInjection/FrameworkExtension.php | 16 +++------------- Resources/config/serializer.php | 2 +- .../FrameworkExtensionTestCase.php | 16 ++++++++++++---- composer.json | 4 ++-- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index bd27c27a6..8d64adeca 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1946,24 +1946,14 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('serializer.default_context', $defaultContext); } - if (!$container->hasDefinition('serializer.normalizer.object')) { - return; - } - - $arguments = $container->getDefinition('serializer.normalizer.object')->getArguments(); - $context = $arguments[6] ?? $defaultContext; - - if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) { - $context += ['circular_reference_handler' => new Reference($config['circular_reference_handler'])]; - $container->getDefinition('serializer.normalizer.object')->setArgument(5, null); + if ($config['circular_reference_handler'] ?? false) { + $container->setParameter('.serializer.circular_reference_handler', $config['circular_reference_handler']); } if ($config['max_depth_handler'] ?? false) { - $context += ['max_depth_handler' => new Reference($config['max_depth_handler'])]; + $container->setParameter('.serializer.max_depth_handler', $config['max_depth_handler']); } - $container->getDefinition('serializer.normalizer.object')->setArgument(6, $context); - $container->getDefinition('serializer.normalizer.property')->setArgument(5, $defaultContext); $container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []); diff --git a/Resources/config/serializer.php b/Resources/config/serializer.php index 4686a88f6..b291f51ac 100644 --- a/Resources/config/serializer.php +++ b/Resources/config/serializer.php @@ -129,7 +129,7 @@ service('property_info')->ignoreOnInvalid(), service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(), null, - null, + abstract_arg('default context, set in the SerializerPass'), service('property_info')->ignoreOnInvalid(), ]) ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -1000]) diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 5f5f41801..7bf66512d 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -33,6 +33,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveTaggedIteratorArgumentPass; @@ -67,6 +68,7 @@ use Symfony\Component\Notifier\TexterInterface; 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; @@ -1447,9 +1449,6 @@ public function testSerializerEnabled() $this->assertEquals(AttributeLoader::class, $argument[0]->getClass()); $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)); - $this->assertArrayHasKey('circular_reference_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6)); - $this->assertArrayHasKey('max_depth_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6)); - $this->assertEquals($container->getDefinition('serializer.normalizer.object')->getArgument(6)['max_depth_handler'], new Reference('my.max.depth.handler')); } public function testSerializerWithoutTranslator() @@ -1547,13 +1546,22 @@ public function testJsonSerializableNormalizerRegistered() public function testObjectNormalizerRegistered() { - $container = $this->createContainerFromFile('full'); + $container = $this->createContainerFromFile('full', compile: false); + $container->addCompilerPass(new SerializerPass()); + $container->addCompilerPass(new ResolveBindingsPass()); + $container->compile(); $definition = $container->getDefinition('serializer.normalizer.object'); $tag = $definition->getTag('serializer.normalizer'); $this->assertEquals(ObjectNormalizer::class, $definition->getClass()); $this->assertEquals(-1000, $tag[0]['priority']); + + $this->assertEquals([ + 'enable_max_depth' => true, + 'circular_reference_handler' => new Reference('my.circular.reference.handler'), + 'max_depth_handler' => new Reference('my.max.depth.handler'), + ], $definition->getArgument(6)); } public function testConstraintViolationListNormalizerRegistered() diff --git a/composer.json b/composer.json index 9b3e7c86e..6689b61b0 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "symfony/scheduler": "^6.4.4|^7.0.4", "symfony/security-bundle": "^6.4|^7.0", "symfony/semaphore": "^6.4|^7.0", - "symfony/serializer": "^7.1", + "symfony/serializer": "^7.2.5", "symfony/stopwatch": "^6.4|^7.0", "symfony/string": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", @@ -97,7 +97,7 @@ "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", "symfony/security-csrf": "<7.2", "symfony/security-core": "<6.4", - "symfony/serializer": "<7.1", + "symfony/serializer": "<7.2.5", "symfony/stopwatch": "<6.4", "symfony/translation": "<6.4", "symfony/twig-bridge": "<6.4", From f1822e2c542c36574b95bd8cb4c37967764ea857 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 24 Oct 2024 19:04:50 +0200 Subject: [PATCH 067/111] [FrameworkBundle] Object Mapper component bindings --- .../Compiler/UnusedTagsPass.php | 2 ++ DependencyInjection/FrameworkExtension.php | 11 +++++++ Resources/config/object_mapper.php | 33 +++++++++++++++++++ Tests/Fixtures/ObjectMapper/ObjectMapped.php | 17 ++++++++++ .../ObjectMapper/ObjectToBeMapped.php | 21 ++++++++++++ .../ObjectMapper/TransformCallable.php | 25 ++++++++++++++ Tests/Functional/ObjectMapperTest.php | 31 +++++++++++++++++ Tests/Functional/app/ObjectMapper/bundles.php | 16 +++++++++ Tests/Functional/app/ObjectMapper/config.yml | 9 +++++ composer.json | 1 + 10 files changed, 166 insertions(+) create mode 100644 Resources/config/object_mapper.php create mode 100644 Tests/Fixtures/ObjectMapper/ObjectMapped.php create mode 100644 Tests/Fixtures/ObjectMapper/ObjectToBeMapped.php create mode 100644 Tests/Fixtures/ObjectMapper/TransformCallable.php create mode 100644 Tests/Functional/ObjectMapperTest.php create mode 100644 Tests/Functional/app/ObjectMapper/bundles.php create mode 100644 Tests/Functional/app/ObjectMapper/config.yml diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index c8271d463..53361e312 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -106,6 +106,8 @@ class UnusedTagsPass implements CompilerPassInterface 'validator.group_provider', 'validator.initializer', 'workflow', + 'object_mapper.transform_callable', + 'object_mapper.condition_callable', ]; public function process(ContainerBuilder $container): void diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index fed06a6c7..5044e3cc2 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -144,6 +144,9 @@ use Symfony\Component\Notifier\Recipient\Recipient; use Symfony\Component\Notifier\TexterInterface; use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface; +use Symfony\Component\ObjectMapper\ConditionCallableInterface; +use Symfony\Component\ObjectMapper\ObjectMapperInterface; +use Symfony\Component\ObjectMapper\TransformCallableInterface; use Symfony\Component\Process\Messenger\RunProcessMessageHandler; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface; @@ -863,6 +866,14 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) { $container->removeDefinition('form.type_extension.upload.validator'); } + + if (ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) { + $loader->load('object_mapper.php'); + $container->registerForAutoconfiguration(TransformCallableInterface::class) + ->addTag('object_mapper.transform_callable'); + $container->registerForAutoconfiguration(ConditionCallableInterface::class) + ->addTag('object_mapper.condition_callable'); + } } private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void diff --git a/Resources/config/object_mapper.php b/Resources/config/object_mapper.php new file mode 100644 index 000000000..8addad4da --- /dev/null +++ b/Resources/config/object_mapper.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface; +use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory; +use Symfony\Component\ObjectMapper\ObjectMapper; +use Symfony\Component\ObjectMapper\ObjectMapperInterface; + +return static function (ContainerConfigurator $container) { + $container->services() + ->set('object_mapper.metadata_factory', ReflectionObjectMapperMetadataFactory::class) + ->alias(ObjectMapperMetadataFactoryInterface::class, 'object_mapper.metadata_factory') + + ->set('object_mapper', ObjectMapper::class) + ->args([ + service('object_mapper.metadata_factory'), + service('property_accessor')->ignoreOnInvalid(), + tagged_locator('object_mapper.transform_callable'), + tagged_locator('object_mapper.condition_callable'), + ]) + ->alias(ObjectMapperInterface::class, 'object_mapper') + ; +}; diff --git a/Tests/Fixtures/ObjectMapper/ObjectMapped.php b/Tests/Fixtures/ObjectMapper/ObjectMapped.php new file mode 100644 index 000000000..17edc9dce --- /dev/null +++ b/Tests/Fixtures/ObjectMapper/ObjectMapped.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper; + +final class ObjectMapped +{ + public string $a; +} diff --git a/Tests/Fixtures/ObjectMapper/ObjectToBeMapped.php b/Tests/Fixtures/ObjectMapper/ObjectToBeMapped.php new file mode 100644 index 000000000..fc5b7080a --- /dev/null +++ b/Tests/Fixtures/ObjectMapper/ObjectToBeMapped.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper; + +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[Map(target: ObjectMapped::class)] +final class ObjectToBeMapped +{ + #[Map(transform: TransformCallable::class)] + public string $a = 'nottransformed'; +} diff --git a/Tests/Fixtures/ObjectMapper/TransformCallable.php b/Tests/Fixtures/ObjectMapper/TransformCallable.php new file mode 100644 index 000000000..da4f26a2d --- /dev/null +++ b/Tests/Fixtures/ObjectMapper/TransformCallable.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper; + +use Symfony\Component\ObjectMapper\TransformCallableInterface; + +/** + * @implements TransformCallableInterface + */ +final class TransformCallable implements TransformCallableInterface +{ + public function __invoke(mixed $value, object $object): mixed + { + return 'transformed'; + } +} diff --git a/Tests/Functional/ObjectMapperTest.php b/Tests/Functional/ObjectMapperTest.php new file mode 100644 index 000000000..e314ee1b0 --- /dev/null +++ b/Tests/Functional/ObjectMapperTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\ObjectMapped; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\ObjectToBeMapped; + +/** + * @author Kévin Dunglas + */ +class ObjectMapperTest extends AbstractWebTestCase +{ + public function testObjectMapper() + { + static::bootKernel(['test_case' => 'ObjectMapper']); + + /** @var Symfony\Component\ObjectMapper\ObjectMapperInterface */ + $objectMapper = static::getContainer()->get('object_mapper.alias'); + $mapped = $objectMapper->map(new ObjectToBeMapped()); + $this->assertSame($mapped->a, 'transformed'); + } +} diff --git a/Tests/Functional/app/ObjectMapper/bundles.php b/Tests/Functional/app/ObjectMapper/bundles.php new file mode 100644 index 000000000..13ab9fdde --- /dev/null +++ b/Tests/Functional/app/ObjectMapper/bundles.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; + +return [ + new FrameworkBundle(), +]; diff --git a/Tests/Functional/app/ObjectMapper/config.yml b/Tests/Functional/app/ObjectMapper/config.yml new file mode 100644 index 000000000..3e3bd8702 --- /dev/null +++ b/Tests/Functional/app/ObjectMapper/config.yml @@ -0,0 +1,9 @@ +imports: + - { resource: ../config/default.yml } + +services: + object_mapper.alias: + alias: object_mapper + public: true + Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\TransformCallable: + autoconfigure: true diff --git a/composer.json b/composer.json index 72d33ed88..6ac3491c7 100644 --- a/composer.json +++ b/composer.json @@ -54,6 +54,7 @@ "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", From 3fca2f034730296f4f4ced6da97c7b903d20ef9b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 26 Mar 2025 09:40:47 +0100 Subject: [PATCH 068/111] =?UTF-8?q?[FrameworkBundle]=C2=A0Add=20missing=20?= =?UTF-8?q?line=20in=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63eca480b..fa7d0a778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.3 --- + * Add support for the ObjectMapper component * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` * Add JsonStreamer services and configuration From c81e1a7882cfc1f37e34aaaf3bfec2eb1c19b1c8 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Fri, 28 Mar 2025 09:22:25 -0400 Subject: [PATCH 069/111] Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()` --- Tests/Console/ApplicationTest.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Tests/Console/ApplicationTest.php b/Tests/Console/ApplicationTest.php index 9afb5a2fd..0b92a813c 100644 --- a/Tests/Console/ApplicationTest.php +++ b/Tests/Console/ApplicationTest.php @@ -135,7 +135,11 @@ public function testRunOnlyWarnsOnUnregistrableCommand() $kernel ->method('getBundles') ->willReturn([$this->createBundleMock( - [(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })] + [(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output): int { + $output->write('fine'); + + return 0; + })] )]); $kernel ->method('getContainer') @@ -163,7 +167,11 @@ public function testRegistrationErrorsAreDisplayedOnCommandNotFound() $kernel ->method('getBundles') ->willReturn([$this->createBundleMock( - [(new Command(null))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })] + [(new Command(null))->setCode(function (InputInterface $input, OutputInterface $output): int { + $output->write('fine'); + + return 0; + })] )]); $kernel ->method('getContainer') @@ -193,7 +201,11 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() $kernel ->method('getBundles') ->willReturn([$this->createBundleMock( - [(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })] + [(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output): int { + $output->write('fine'); + + return 0; + })] )]); $kernel ->method('getContainer') From 8d83a9020ad88075f2abd655e1819359bd56a636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 28 Mar 2025 23:25:48 +0100 Subject: [PATCH 070/111] Enable controller service with #[Route] attribute instead of #[AsController] --- CHANGELOG.md | 2 ++ DependencyInjection/FrameworkExtension.php | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7d0a778..997564262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ CHANGELOG * Add DI alias from `ServicesResetterInterface` to `services_resetter` * Add `methods` argument in `#[IsCsrfTokenValid]` attribute * Allow configuring the logging channel per type of exceptions + * Enable service argument resolution on classes that use the `#[Route]` attribute, + the `#[AsController]` attribute is no longer required 7.2 --- diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 0015aee41..7e500af88 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -164,6 +164,7 @@ use Symfony\Component\RateLimiter\Storage\CacheStorage; use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer; use Symfony\Component\RemoteEvent\RemoteEvent; +use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Scheduler\Attribute\AsCronTask; use Symfony\Component\Scheduler\Attribute\AsPeriodicTask; use Symfony\Component\Scheduler\Attribute\AsSchedule; @@ -429,7 +430,7 @@ public function load(array $configs, ContainerBuilder $container): void } $loggers[$exception['log_channel']] = new Reference('monolog.logger.'.$exception['log_channel'], ContainerInterface::NULL_ON_INVALID_REFERENCE); } - + $exceptionListener ->replaceArgument(3, $config['exceptions']) ->setArgument(4, $loggers) @@ -739,6 +740,9 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void { $definition->addTag('controller.service_arguments'); }); + $container->registerAttributeForAutoconfiguration(Route::class, static function (ChildDefinition $definition, Route $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { + $definition->addTag('controller.service_arguments'); + }); $container->registerAttributeForAutoconfiguration(AsRemoteEventConsumer::class, static function (ChildDefinition $definition, AsRemoteEventConsumer $attribute): void { $definition->addTag('remote_event.consumer', ['consumer' => $attribute->name]); }); From 33a91b7bc9957ca6b988fe5db0c7554d4358d3f1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 31 Mar 2025 14:55:09 +0200 Subject: [PATCH 071/111] [FrameworkBundle] Exclude validator constrains, attributes, enums from the container --- DependencyInjection/FrameworkExtension.php | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 7e500af88..d76aa8cd6 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -204,6 +204,7 @@ use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\GroupProviderInterface; @@ -786,29 +787,34 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } $container->registerForAutoconfiguration(CompilerPassInterface::class) - ->addTag('container.excluded.compiler_pass')->addTag('container.excluded')->setAbstract(true); + ->addTag('container.excluded', ['source' => 'because it\'s a compiler pass'])->setAbstract(true); + $container->registerForAutoconfiguration(Constraint::class) + ->addTag('container.excluded', ['source' => 'because it\'s a validation constraint'])->setAbstract(true); $container->registerForAutoconfiguration(TestCase::class) - ->addTag('container.excluded.test_case')->addTag('container.excluded')->setAbstract(true); + ->addTag('container.excluded', ['source' => 'because it\'s a test case'])->setAbstract(true); + $container->registerForAutoconfiguration(\UnitEnum::class) + ->addTag('container.excluded', ['source' => 'because it\'s an enum'])->setAbstract(true); $container->registerAttributeForAutoconfiguration(AsMessage::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded.messenger.message')->addTag('container.excluded')->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a messenger message'])->setAbstract(true); + }); + $container->registerAttributeForAutoconfiguration(\Attribute::class, static function (ChildDefinition $definition) { + $definition->addTag('container.excluded', ['source' => 'because it\'s an attribute'])->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(Entity::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded.doctrine.entity')->addTag('container.excluded')->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a doctrine entity'])->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(Embeddable::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded.doctrine.embeddable')->addTag('container.excluded')->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a doctrine embeddable'])->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(MappedSuperclass::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded.doctrine.mapped_superclass')->addTag('container.excluded')->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a doctrine mapped superclass'])->setAbstract(true); }); $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute) { $definition->addTag('json_streamer.streamable', [ 'object' => $attribute->asObject, 'list' => $attribute->asList, - ]); - $definition->addTag('container.excluded'); - $definition->setAbstract(true); + ])->addTag('container.excluded', ['source' => 'because it\'s a streamable JSON'])->setAbstract(true); }); if (!$container->getParameter('kernel.debug')) { From ea981cce0ee667131b4a50c03448aa23cca74ff2 Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Fri, 28 Mar 2025 09:42:19 +0100 Subject: [PATCH 072/111] [FrameworkBundle] Deprecate setting the `collect_serializer_data` to `false` --- CHANGELOG.md | 1 + DependencyInjection/FrameworkExtension.php | 4 ++++ .../DependencyInjection/Fixtures/php/profiler.php | 1 + .../php/profiler_collect_serializer_data.php | 15 --------------- .../DependencyInjection/Fixtures/xml/profiler.xml | 2 +- .../xml/profiler_collect_serializer_data.xml | 15 --------------- .../DependencyInjection/Fixtures/yml/profiler.yml | 1 + .../yml/profiler_collect_serializer_data.yml | 11 ----------- .../FrameworkExtensionTestCase.php | 11 +---------- Tests/Functional/app/config/framework.yml | 2 ++ 10 files changed, 11 insertions(+), 52 deletions(-) delete mode 100644 Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php delete mode 100644 Tests/DependencyInjection/Fixtures/xml/profiler_collect_serializer_data.xml delete mode 100644 Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 997564262..6c4daeb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ CHANGELOG * Allow configuring the logging channel per type of exceptions * Enable service argument resolution on classes that use the `#[Route]` attribute, the `#[AsController]` attribute is no longer required + * Deprecate setting the `framework.profiler.collect_serializer_data` config option to `false` 7.2 --- diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 7e500af88..f6440e3de 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -988,6 +988,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $loader->load('notifier_debug.php'); } + if (false === $config['collect_serializer_data']) { + trigger_deprecation('symfony/framework-bundle', '7.3', 'Setting the "framework.profiler.collect_serializer_data" config option to "false" is deprecated.'); + } + if ($this->isInitializedConfigEnabled('serializer') && $config['collect_serializer_data']) { $loader->load('serializer_debug.php'); } diff --git a/Tests/DependencyInjection/Fixtures/php/profiler.php b/Tests/DependencyInjection/Fixtures/php/profiler.php index faf76bbc7..99e2a52cf 100644 --- a/Tests/DependencyInjection/Fixtures/php/profiler.php +++ b/Tests/DependencyInjection/Fixtures/php/profiler.php @@ -7,6 +7,7 @@ 'php_errors' => ['log' => true], 'profiler' => [ 'enabled' => true, + 'collect_serializer_data' => true, ], 'serializer' => [ 'enabled' => true, diff --git a/Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php b/Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php deleted file mode 100644 index 99e2a52cf..000000000 --- a/Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php +++ /dev/null @@ -1,15 +0,0 @@ -loadFromExtension('framework', [ - 'annotations' => false, - 'http_method_override' => false, - 'handle_all_throwables' => true, - 'php_errors' => ['log' => true], - 'profiler' => [ - 'enabled' => true, - 'collect_serializer_data' => true, - ], - 'serializer' => [ - 'enabled' => true, - ], -]); diff --git a/Tests/DependencyInjection/Fixtures/xml/profiler.xml b/Tests/DependencyInjection/Fixtures/xml/profiler.xml index ffbff7f21..34d44d91c 100644 --- a/Tests/DependencyInjection/Fixtures/xml/profiler.xml +++ b/Tests/DependencyInjection/Fixtures/xml/profiler.xml @@ -9,7 +9,7 @@ - + diff --git a/Tests/DependencyInjection/Fixtures/xml/profiler_collect_serializer_data.xml b/Tests/DependencyInjection/Fixtures/xml/profiler_collect_serializer_data.xml deleted file mode 100644 index 34d44d91c..000000000 --- a/Tests/DependencyInjection/Fixtures/xml/profiler_collect_serializer_data.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/Tests/DependencyInjection/Fixtures/yml/profiler.yml b/Tests/DependencyInjection/Fixtures/yml/profiler.yml index 5c867fc89..2ccec1685 100644 --- a/Tests/DependencyInjection/Fixtures/yml/profiler.yml +++ b/Tests/DependencyInjection/Fixtures/yml/profiler.yml @@ -6,5 +6,6 @@ framework: log: true profiler: enabled: true + collect_serializer_data: true serializer: enabled: true diff --git a/Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml b/Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml deleted file mode 100644 index 5fe74b290..000000000 --- a/Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml +++ /dev/null @@ -1,11 +0,0 @@ -framework: - annotations: false - http_method_override: false - handle_all_throwables: true - php_errors: - log: true - serializer: - enabled: true - profiler: - enabled: true - collect_serializer_data: true diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 8bddf53be..d942c122c 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -278,22 +278,13 @@ public function testDisabledProfiler() public function testProfilerCollectSerializerDataEnabled() { - $container = $this->createContainerFromFile('profiler_collect_serializer_data'); + $container = $this->createContainerFromFile('profiler'); $this->assertTrue($container->hasDefinition('profiler')); $this->assertTrue($container->hasDefinition('serializer.data_collector')); $this->assertTrue($container->hasDefinition('debug.serializer')); } - public function testProfilerCollectSerializerDataDefaultDisabled() - { - $container = $this->createContainerFromFile('profiler'); - - $this->assertTrue($container->hasDefinition('profiler')); - $this->assertFalse($container->hasDefinition('serializer.data_collector')); - $this->assertFalse($container->hasDefinition('debug.serializer')); - } - public function testWorkflows() { $container = $this->createContainerFromFile('workflows'); diff --git a/Tests/Functional/app/config/framework.yml b/Tests/Functional/app/config/framework.yml index 1eaee513c..ac051614b 100644 --- a/Tests/Functional/app/config/framework.yml +++ b/Tests/Functional/app/config/framework.yml @@ -18,6 +18,8 @@ framework: cookie_samesite: lax php_errors: log: true + profiler: + collect_serializer_data: true services: logger: { class: Psr\Log\NullLogger } From d4d23025226aff76e03259f6979dfa3db0db1d07 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 31 Mar 2025 11:48:18 -0400 Subject: [PATCH 073/111] [FrameworkBundle][RateLimiter] default `lock_factory` to `auto` --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 2 +- DependencyInjection/FrameworkExtension.php | 4 +++ .../PhpFrameworkExtensionTest.php | 31 +++++++++++++++++-- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4daeb6d..b7efe5a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ CHANGELOG * Enable service argument resolution on classes that use the `#[Route]` attribute, the `#[AsController]` attribute is no longer required * Deprecate setting the `framework.profiler.collect_serializer_data` config option to `false` + * Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index aa61cb12c..7f37b5216 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2504,7 +2504,7 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->children() ->scalarNode('lock_factory') ->info('The service ID of the lock factory used by this limiter (or null to disable locking).') - ->defaultValue('lock.factory') + ->defaultValue('auto') ->end() ->scalarNode('cache_pool') ->info('The cache pool to use for storing the current limiter state.') diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 1a1bcdd16..98e2e8904 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -3239,6 +3239,10 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde $limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter')) ->addTag('rate_limiter', ['name' => $name]); + if ('auto' === $limiterConfig['lock_factory']) { + $limiterConfig['lock_factory'] = $this->isInitializedConfigEnabled('lock') ? 'lock.factory' : null; + } + if (null !== $limiterConfig['lock_factory']) { if (!interface_exists(LockInterface::class)) { throw new LogicException(\sprintf('Rate limiter "%s" requires the Lock component to be installed. Try running "composer require symfony/lock".', $name)); diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index deac159b6..ea8d481e0 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -188,7 +188,7 @@ public function testWorkflowDefaultMarkingStoreDefinition() $this->assertNull($argumentsB['index_1'], 'workflow_b marking_store argument is null'); } - public function testRateLimiterWithLockFactory() + public function testRateLimiterLockFactoryWithLockDisabled() { try { $this->createContainerFromClosure(function (ContainerBuilder $container) { @@ -199,7 +199,7 @@ public function testRateLimiterWithLockFactory() 'php_errors' => ['log' => true], 'lock' => false, 'rate_limiter' => [ - 'with_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + 'with_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour', 'lock_factory' => 'lock.factory'], ], ]); }); @@ -208,7 +208,10 @@ public function testRateLimiterWithLockFactory() } catch (LogicException $e) { $this->assertEquals('Rate limiter "with_lock" requires the Lock component to be configured.', $e->getMessage()); } + } + public function testRateLimiterAutoLockFactoryWithLockEnabled() + { $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { $container->loadFromExtension('framework', [ 'annotations' => false, @@ -226,13 +229,35 @@ public function testRateLimiterWithLockFactory() $this->assertEquals('lock.factory', (string) $withLock->getArgument(2)); } - public function testRateLimiterLockFactory() + public function testRateLimiterAutoLockFactoryWithLockDisabled() { $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { $container->loadFromExtension('framework', [ 'annotations' => false, 'http_method_override' => false, 'handle_all_throwables' => true, + 'lock' => false, + 'php_errors' => ['log' => true], + 'rate_limiter' => [ + 'without_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + ], + ]); + }); + + $this->expectException(OutOfBoundsException::class); + $this->expectExceptionMessageMatches('/^The argument "2" doesn\'t exist.*\.$/'); + + $container->getDefinition('limiter.without_lock')->getArgument(2); + } + + public function testRateLimiterDisableLockFactory() + { + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'lock' => true, 'php_errors' => ['log' => true], 'rate_limiter' => [ 'without_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour', 'lock_factory' => null], From eab16fe6e61f0df51c30ca2589d8cf3f585633b8 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Wed, 12 Feb 2025 19:13:00 +0100 Subject: [PATCH 074/111] [Config] Add `NodeDefinition::docUrl()` --- Command/ConfigDebugCommand.php | 15 +++++++++++++++ Command/ConfigDumpReferenceCommand.php | 19 +++++++++++++++++++ DependencyInjection/Configuration.php | 1 + 3 files changed, 35 insertions(+) diff --git a/Command/ConfigDebugCommand.php b/Command/ConfigDebugCommand.php index 55c101e9c..8d5f85cee 100644 --- a/Command/ConfigDebugCommand.php +++ b/Command/ConfigDebugCommand.php @@ -104,6 +104,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->title( \sprintf('Current configuration for %s', $name === $extensionAlias ? \sprintf('extension with alias "%s"', $extensionAlias) : \sprintf('"%s"', $name)) ); + + if ($docUrl = $this->getDocUrl($extension, $container)) { + $io->comment(\sprintf('Documentation at %s', $docUrl)); + } } $io->writeln($this->convertToFormat([$extensionAlias => $config], $format)); @@ -269,4 +273,15 @@ private function getAvailableFormatOptions(): array { return ['txt', 'yaml', 'json']; } + + private function getDocUrl(ExtensionInterface $extension, ContainerBuilder $container): ?string + { + $configuration = $extension instanceof ConfigurationInterface ? $extension : $extension->getConfiguration($container->getExtensionConfig($extension->getAlias()), $container); + + return $configuration + ->getConfigTreeBuilder() + ->getRootNode() + ->getNode(true) + ->getAttribute('docUrl'); + } } diff --git a/Command/ConfigDumpReferenceCommand.php b/Command/ConfigDumpReferenceCommand.php index 7e5cd765f..3cb744d74 100644 --- a/Command/ConfigDumpReferenceCommand.php +++ b/Command/ConfigDumpReferenceCommand.php @@ -23,6 +23,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\Yaml\Yaml; /** @@ -123,6 +124,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $message .= \sprintf(' at path "%s"', $path); } + if ($docUrl = $this->getExtensionDocUrl($extension)) { + $message .= \sprintf(' (see %s)', $docUrl); + } + switch ($format) { case 'yaml': $io->writeln(\sprintf('# %s', $message)); @@ -182,4 +187,18 @@ private function getAvailableFormatOptions(): array { return ['yaml', 'xml']; } + + private function getExtensionDocUrl(ConfigurationInterface|ConfigurationExtensionInterface $extension): ?string + { + $kernel = $this->getApplication()->getKernel(); + $container = $this->getContainerBuilder($kernel); + + $configuration = $extension instanceof ConfigurationInterface ? $extension : $extension->getConfiguration($container->getExtensionConfig($extension->getAlias()), $container); + + return $configuration + ->getConfigTreeBuilder() + ->getRootNode() + ->getNode(true) + ->getAttribute('docUrl'); + } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index aa61cb12c..0f882d356 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -75,6 +75,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode = $treeBuilder->getRootNode(); $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) { From 3a074477a48991c11d8441e490187e74ce85720c Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 7 Apr 2025 16:05:31 -0400 Subject: [PATCH 075/111] [FrameworkBundle][RateLimiter] deprecate `RateLimiterFactory` alias --- CHANGELOG.md | 1 + DependencyInjection/FrameworkExtension.php | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7efe5a18..2f145d165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ CHANGELOG the `#[AsController]` attribute is no longer required * Deprecate setting the `framework.profiler.collect_serializer_data` config option to `false` * Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default + * Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead 7.2 --- diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 98e2e8904..814397d9c 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -3266,10 +3266,11 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde $limiterConfig['id'] = $name; $limiter->replaceArgument(0, $limiterConfig); - $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); + $factoryAlias = $container->registerAliasForArgument($limiterId, RateLimiterFactory::class, $name.'.limiter'); 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.'); } } } From 80403e0cceec80bdfd33e958e3519615218eaab0 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 5 Apr 2025 10:05:38 -0400 Subject: [PATCH 076/111] [FrameworkBundle][RateLimiter] compound rate limiter config --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 11 ++- DependencyInjection/FrameworkExtension.php | 37 +++++++++ .../PhpFrameworkExtensionTest.php | 75 +++++++++++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f145d165..8e70fb98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ CHANGELOG * Deprecate setting the `framework.profiler.collect_serializer_data` config option to `false` * Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default * Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead + * Allow configuring compound rate limiters 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 6dc1b7d6e..6b168a2d4 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2518,7 +2518,12 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->enumNode('policy') ->info('The algorithm to be used by this limiter.') ->isRequired() - ->values(['fixed_window', 'token_bucket', 'sliding_window', 'no_limit']) + ->values(['fixed_window', 'token_bucket', 'sliding_window', 'compound', 'no_limit']) + ->end() + ->arrayNode('limiters') + ->info('The limiter names to use when using the "compound" policy.') + ->beforeNormalization()->castToArray()->end() + ->scalarPrototype()->end() ->end() ->integerNode('limit') ->info('The maximum allowed hits in a fixed interval or burst.') @@ -2537,8 +2542,8 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $ ->end() ->end() ->validate() - ->ifTrue(fn ($v) => 'no_limit' !== $v['policy'] && !isset($v['limit'])) - ->thenInvalid('A limit must be provided when using a policy different than "no_limit".') + ->ifTrue(static fn ($v) => !\in_array($v['policy'], ['no_limit', 'compound']) && !isset($v['limit'])) + ->thenInvalid('A limit must be provided when using a policy different than "compound" or "no_limit".') ->end() ->end() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 814397d9c..716c11b63 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -59,6 +59,7 @@ use Symfony\Component\Console\Debug\CliRequest; use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -158,6 +159,7 @@ use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\RateLimiter\CompoundRateLimiterFactory; use Symfony\Component\RateLimiter\LimiterInterface; use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; @@ -3232,7 +3234,18 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde { $loader->load('rate_limiter.php'); + $limiters = []; + $compoundLimiters = []; + foreach ($config['limiters'] as $name => $limiterConfig) { + if ('compound' === $limiterConfig['policy']) { + $compoundLimiters[$name] = $limiterConfig; + + continue; + } + + $limiters[] = $name; + // default configuration (when used by other DI extensions) $limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter']; @@ -3273,6 +3286,30 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde $factoryAlias->setDeprecated('symfony/dependency-injection', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.'); } } + + if ($compoundLimiters && !class_exists(CompoundRateLimiterFactory::class)) { + throw new LogicException('Configuring compound rate limiters is only available in symfony/rate-limiter 7.3+.'); + } + + foreach ($compoundLimiters as $name => $limiterConfig) { + if (!$limiterConfig['limiters']) { + throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter.', $name)); + } + + 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( + static fn (string $name) => new Reference('limiter.'.$name), + $limiterConfig['limiters'] + ))) + ; + + $container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter'); + } } private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index ea8d481e0..a7606b683 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\RateLimiter\CompoundRateLimiterFactory; +use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase @@ -290,4 +292,77 @@ public function testRateLimiterIsTagged() $this->assertSame('first', $container->getDefinition('limiter.first')->getTag('rate_limiter')[0]['name']); $this->assertSame('second', $container->getDefinition('limiter.second')->getTag('rate_limiter')[0]['name']); } + + public function testRateLimiterCompoundPolicy() + { + if (!class_exists(CompoundRateLimiterFactory::class)) { + $this->markTestSkipped('CompoundRateLimiterFactory is not available.'); + } + + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'lock' => true, + 'rate_limiter' => [ + 'first' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'], + 'second' => ['policy' => 'sliding_window', 'limit' => 10, 'interval' => '1 hour'], + 'compound' => ['policy' => 'compound', 'limiters' => ['first', 'second']], + ], + ]); + }); + + $definition = $container->getDefinition('limiter.compound'); + $this->assertSame(CompoundRateLimiterFactory::class, $definition->getClass()); + $this->assertEquals( + [ + 'limiter.first', + 'limiter.second', + ], + $definition->getArgument(0)->getValues() + ); + $this->assertSame('limiter.compound', (string) $container->getAlias(RateLimiterFactoryInterface::class.' $compoundLimiter')); + } + + public function testRateLimiterCompoundPolicyNoLimiters() + { + if (!class_exists(CompoundRateLimiterFactory::class)) { + $this->markTestSkipped('CompoundRateLimiterFactory is not available.'); + } + + $this->expectException(\LogicException::class); + $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'rate_limiter' => [ + 'compound' => ['policy' => 'compound'], + ], + ]); + }); + } + + public function testRateLimiterCompoundPolicyInvalidLimiters() + { + if (!class_exists(CompoundRateLimiterFactory::class)) { + $this->markTestSkipped('CompoundRateLimiterFactory is not available.'); + } + + $this->expectException(\LogicException::class); + $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'rate_limiter' => [ + 'compound' => ['policy' => 'compound', 'limiters' => ['invalid1', 'invalid2']], + ], + ]); + }); + } } From acdab36a61f1d2cf2dad249843b1704405400546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 19 Mar 2025 10:57:26 +0100 Subject: [PATCH 077/111] [Messenger] Reset peak memory usage for each message --- Resources/config/messenger.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Resources/config/messenger.php b/Resources/config/messenger.php index 8798d5f2e..e02cd1ca3 100644 --- a/Resources/config/messenger.php +++ b/Resources/config/messenger.php @@ -18,6 +18,7 @@ use Symfony\Component\Messenger\Bridge\Redis\Transport\RedisTransportFactory; use Symfony\Component\Messenger\EventListener\AddErrorDetailsStampListener; use Symfony\Component\Messenger\EventListener\DispatchPcntlSignalListener; +use Symfony\Component\Messenger\EventListener\ResetMemoryUsageListener; use Symfony\Component\Messenger\EventListener\ResetServicesListener; use Symfony\Component\Messenger\EventListener\SendFailedMessageForRetryListener; use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener; @@ -218,6 +219,9 @@ service('services_resetter'), ]) + ->set('messenger.listener.reset_memory_usage', ResetMemoryUsageListener::class) + ->tag('kernel.event_subscriber') + ->set('messenger.routable_message_bus', RoutableMessageBus::class) ->args([ abstract_arg('message bus locator'), From 0839cc6be4615e815c41ad94e15a35720e0de791 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 9 Apr 2025 10:47:56 +0200 Subject: [PATCH 078/111] remove service if its class does not exist --- DependencyInjection/FrameworkExtension.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 716c11b63..5595e14b3 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -126,6 +126,7 @@ use Symfony\Component\Messenger\Attribute\AsMessage; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Bridge as MessengerBridge; +use Symfony\Component\Messenger\EventListener\ResetMemoryUsageListener; use Symfony\Component\Messenger\Handler\BatchHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; @@ -2304,6 +2305,10 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.normalizer.flatten_exception'); } + if (!class_exists(ResetMemoryUsageListener::class)) { + $container->removeDefinition('messenger.listener.reset_memory_usage'); + } + if (ContainerBuilder::willBeAvailable('symfony/amqp-messenger', MessengerBridge\Amqp\Transport\AmqpTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) { $container->getDefinition('messenger.transport.amqp.factory')->addTag('messenger.transport_factory'); } From 962751f8d4769f2203dd38bf7f8077c87c719085 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Tue, 15 Apr 2025 11:04:08 -0400 Subject: [PATCH 079/111] [HttpFoundation][FrameworkBundle] clock support for `UriSigner` --- Resources/config/services.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Resources/config/services.php b/Resources/config/services.php index 558c2b6d5..936867d54 100644 --- a/Resources/config/services.php +++ b/Resources/config/services.php @@ -158,6 +158,9 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->set('uri_signer', UriSigner::class) ->args([ new Parameter('kernel.secret'), + '_hash', + '_expiration', + service('clock')->nullOnInvalid(), ]) ->lazy() ->alias(UriSigner::class, 'uri_signer') From 4bd6301f4de3baa50f6e5f2149fcd7433d9fc69a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 18 Apr 2025 14:51:48 +0200 Subject: [PATCH 080/111] Don't enable tracing unless the profiler is enabled --- DependencyInjection/FrameworkExtension.php | 6 ++++++ Resources/config/debug.php | 1 + Resources/config/profiling.php | 11 +++++++++++ Resources/config/validator_debug.php | 1 + 4 files changed, 19 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 5595e14b3..f5111cd10 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -106,6 +106,7 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; +use Symfony\Component\HttpKernel\Profiler\ProfilerStateChecker; use Symfony\Component\JsonStreamer\Attribute\JsonStreamable; use Symfony\Component\JsonStreamer\JsonStreamWriter; use Symfony\Component\JsonStreamer\StreamReaderInterface; @@ -963,6 +964,11 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $loader->load('collectors.php'); $loader->load('cache_debug.php'); + if (!class_exists(ProfilerStateChecker::class)) { + $container->removeDefinition('profiler.state_checker'); + $container->removeDefinition('profiler.is_disabled_state_checker'); + } + if ($this->isInitializedConfigEnabled('form')) { $loader->load('form_debug.php'); } diff --git a/Resources/config/debug.php b/Resources/config/debug.php index 5c426653d..842f5b35b 100644 --- a/Resources/config/debug.php +++ b/Resources/config/debug.php @@ -25,6 +25,7 @@ service('debug.stopwatch'), service('logger')->nullOnInvalid(), service('.virtual_request_stack')->nullOnInvalid(), + service('profiler.is_disabled_state_checker')->nullOnInvalid(), ]) ->tag('monolog.logger', ['channel' => 'event']) ->tag('kernel.reset', ['method' => 'reset']) diff --git a/Resources/config/profiling.php b/Resources/config/profiling.php index 4ae34649b..68fb295bb 100644 --- a/Resources/config/profiling.php +++ b/Resources/config/profiling.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\HttpKernel\Profiler\ProfilerStateChecker; return static function (ContainerConfigurator $container) { $container->services() @@ -56,5 +57,15 @@ ->set('.virtual_request_stack', VirtualRequestStack::class) ->args([service('request_stack')]) ->public() + + ->set('profiler.state_checker', ProfilerStateChecker::class) + ->args([ + service_locator(['profiler' => service('profiler')->ignoreOnUninitialized()]), + param('kernel.runtime_mode.web'), + ]) + + ->set('profiler.is_disabled_state_checker', 'Closure') + ->factory(['Closure', 'fromCallable']) + ->args([[service('profiler.state_checker'), 'isProfilerDisabled']]) ; }; diff --git a/Resources/config/validator_debug.php b/Resources/config/validator_debug.php index e9fe44114..b195aea2b 100644 --- a/Resources/config/validator_debug.php +++ b/Resources/config/validator_debug.php @@ -20,6 +20,7 @@ ->decorate('validator', null, 255) ->args([ service('debug.validator.inner'), + service('profiler.is_disabled_state_checker')->nullOnInvalid(), ]) ->tag('kernel.reset', [ 'method' => 'reset', From 0feb2e31e5fc040b15a57823151ede071b3e8212 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 17 Apr 2025 11:41:39 +0200 Subject: [PATCH 081/111] Add PHP config support for routing --- CHANGELOG.md | 1 + Resources/config/routing/errors.php | 20 ++++++++++++++++++++ Resources/config/routing/errors.xml | 6 +----- Resources/config/routing/webhook.php | 19 +++++++++++++++++++ Resources/config/routing/webhook.xml | 5 +---- 5 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 Resources/config/routing/errors.php create mode 100644 Resources/config/routing/webhook.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e70fb98e..40289cf57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.3 --- + * Add `errors.php` and `webhook.php` routing configuration files (use them instead of their XML equivalent) * Add support for the ObjectMapper component * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` diff --git a/Resources/config/routing/errors.php b/Resources/config/routing/errors.php new file mode 100644 index 000000000..11040e29a --- /dev/null +++ b/Resources/config/routing/errors.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + +return function (RoutingConfigurator $routes): void { + $routes->add('_preview_error', '/{code}.{_format}') + ->controller('error_controller::preview') + ->defaults(['_format' => 'html']) + ->requirements(['code' => '\d+']) + ; +}; diff --git a/Resources/config/routing/errors.xml b/Resources/config/routing/errors.xml index 13a9cc407..f890aef1e 100644 --- a/Resources/config/routing/errors.xml +++ b/Resources/config/routing/errors.xml @@ -4,9 +4,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - - error_controller::preview - html - \d+ - + diff --git a/Resources/config/routing/webhook.php b/Resources/config/routing/webhook.php new file mode 100644 index 000000000..413fe6c81 --- /dev/null +++ b/Resources/config/routing/webhook.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + +return function (RoutingConfigurator $routes): void { + $routes->add('_webhook_controller', '/{type}') + ->controller('webhook_controller::handle') + ->requirements(['type' => '.+']) + ; +}; diff --git a/Resources/config/routing/webhook.xml b/Resources/config/routing/webhook.xml index dfa95cfac..8cb64ebb7 100644 --- a/Resources/config/routing/webhook.xml +++ b/Resources/config/routing/webhook.xml @@ -4,8 +4,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> - - webhook.controller::handle - .+ - + From d0b06133b00e4dd3df7f47a3188fb7baabcc6b2a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 27 Apr 2025 15:26:02 +0200 Subject: [PATCH 082/111] Remove unneeded use statements --- Command/TranslationUpdateCommand.php | 1 - Tests/DependencyInjection/ConfigurationTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Command/TranslationUpdateCommand.php b/Command/TranslationUpdateCommand.php index 0ffe6a949..f8ce99c41 100644 --- a/Command/TranslationUpdateCommand.php +++ b/Command/TranslationUpdateCommand.php @@ -19,7 +19,6 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\KernelInterface; diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 171cfedc4..76d135122 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -13,7 +13,6 @@ use Doctrine\DBAL\Connection; use PHPUnit\Framework\TestCase; -use Seld\JsonLint\JsonParser; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration; use Symfony\Bundle\FullStack; use Symfony\Component\Cache\Adapter\DoctrineAdapter; From decd9023a9e52695607b7a1bafc8c7b51fc3d900 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 28 Apr 2025 12:59:59 +0200 Subject: [PATCH 083/111] fix the upgrade instructions and trigger deprecations --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ Resources/config/routing/errors.php | 11 +++++++++++ Resources/config/routing/webhook.php | 11 +++++++++++ 3 files changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40289cf57..935479f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ CHANGELOG --- * Add `errors.php` and `webhook.php` routing configuration files (use them instead of their XML equivalent) + + Before: + + ```yaml + when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + prefix: /_error + + webhook: + resource: '@FrameworkBundle/Resources/config/routing/webhook.xml' + prefix: /webhook + ``` + + After: + + ```yaml + when@dev: + _errors: + resource: '@FrameworkBundle/Resources/config/routing/errors.php' + prefix: /_error + + webhook: + resource: '@FrameworkBundle/Resources/config/routing/webhook.php' + prefix: /webhook + ``` + * Add support for the ObjectMapper component * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` diff --git a/Resources/config/routing/errors.php b/Resources/config/routing/errors.php index 11040e29a..36a46dee4 100644 --- a/Resources/config/routing/errors.php +++ b/Resources/config/routing/errors.php @@ -10,8 +10,19 @@ */ use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Routing\Loader\XmlFileLoader; return function (RoutingConfigurator $routes): void { + foreach (debug_backtrace() as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) { + if (__DIR__ === dirname(realpath($trace['args'][3]))) { + trigger_deprecation('symfony/routing', '7.3', 'The "errors.xml" routing configuration file is deprecated, import "errors.php" instead.'); + + break; + } + } + } + $routes->add('_preview_error', '/{code}.{_format}') ->controller('error_controller::preview') ->defaults(['_format' => 'html']) diff --git a/Resources/config/routing/webhook.php b/Resources/config/routing/webhook.php index 413fe6c81..ea8031159 100644 --- a/Resources/config/routing/webhook.php +++ b/Resources/config/routing/webhook.php @@ -10,8 +10,19 @@ */ use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Routing\Loader\XmlFileLoader; return function (RoutingConfigurator $routes): void { + foreach (debug_backtrace() as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof XmlFileLoader && 'doImport' === $trace['function']) { + if (__DIR__ === dirname(realpath($trace['args'][3]))) { + trigger_deprecation('symfony/routing', '7.3', 'The "webhook.xml" routing configuration file is deprecated, import "webhook.php" instead.'); + + break; + } + } + } + $routes->add('_webhook_controller', '/{type}') ->controller('webhook_controller::handle') ->requirements(['type' => '.+']) From 7ed440308439b894f21630e85caeaf212c74849d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 2 May 2025 07:43:50 +0200 Subject: [PATCH 084/111] drop the limiters option for non-compound rater limiters --- DependencyInjection/FrameworkExtension.php | 2 ++ .../PhpFrameworkExtensionTest.php | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f5111cd10..2dd6ed95e 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -3255,6 +3255,8 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde continue; } + unset($limiterConfig['limiters']); + $limiters[] = $name; // default configuration (when used by other DI extensions) diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index a7606b683..60a1765f7 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -314,6 +314,19 @@ public function testRateLimiterCompoundPolicy() ]); }); + $this->assertSame([ + 'policy' => 'fixed_window', + 'limit' => 10, + 'interval' => '1 hour', + 'id' => 'first', + ], $container->getDefinition('limiter.first')->getArgument(0)); + $this->assertSame([ + 'policy' => 'sliding_window', + 'limit' => 10, + 'interval' => '1 hour', + 'id' => 'second', + ], $container->getDefinition('limiter.second')->getArgument(0)); + $definition = $container->getDefinition('limiter.compound'); $this->assertSame(CompoundRateLimiterFactory::class, $definition->getClass()); $this->assertEquals( From 0b95b03a5cb65816004d61b6a4d9a38e5cefeebf Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Fri, 2 May 2025 20:47:36 +0200 Subject: [PATCH 085/111] [Mailer][Mime] Update SMIME repository node description in configuration Clarified the documentation for the S/MIME certificate repository configuration. It now specifies that the repository should be a service implementing `SmimeCertificateRepositoryInterface`. --- DependencyInjection/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 6b168a2d4..51db38963 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2350,7 +2350,7 @@ private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enabl ->info('S/MIME encrypter configuration') ->children() ->scalarNode('repository') - ->info('Path to the S/MIME certificate repository. Shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`.') + ->info('S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`.') ->defaultValue('') ->cannotBeEmpty() ->end() From 5b3a4cf82f21e6e96ef231a06e6539249483267a Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 2 Apr 2025 09:54:43 +0200 Subject: [PATCH 086/111] [ObjectMapper] Condition to target a specific class --- Tests/Fixtures/ObjectMapper/TransformCallable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Fixtures/ObjectMapper/TransformCallable.php b/Tests/Fixtures/ObjectMapper/TransformCallable.php index da4f26a2d..3321e28d1 100644 --- a/Tests/Fixtures/ObjectMapper/TransformCallable.php +++ b/Tests/Fixtures/ObjectMapper/TransformCallable.php @@ -18,7 +18,7 @@ */ final class TransformCallable implements TransformCallableInterface { - public function __invoke(mixed $value, object $object): mixed + public function __invoke(mixed $value, object $source, ?object $target): mixed { return 'transformed'; } From 017a7a464ee817262bb51df820679b9014f2c799 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 6 May 2025 20:24:47 +0200 Subject: [PATCH 087/111] bump min constraint for the ObjectMapper component --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2ecedbc45..bc312827f 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": "^7.3", + "symfony/object-mapper": "^v7.3.0-beta2", "symfony/process": "^6.4|^7.0", "symfony/rate-limiter": "^6.4|^7.0", "symfony/scheduler": "^6.4.4|^7.0.4", From e14c287b356b6c30a99fa7862c988e3a4a3ba55d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 6 May 2025 22:43:17 +0200 Subject: [PATCH 088/111] ensure that all supported e-mail validation modes can be configured --- DependencyInjection/Configuration.php | 3 +- .../PhpFrameworkExtensionTest.php | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index cb52a0704..4d44c469f 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -45,6 +45,7 @@ use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\Translator; use Symfony\Component\Uid\Factory\UuidFactory; +use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Validation; use Symfony\Component\Webhook\Controller\WebhookController; use Symfony\Component\WebLink\HttpHeaderSerializer; @@ -1066,7 +1067,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->validate()->castToArray()->end() ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() - ->enumNode('email_validation_mode')->values(['html5', 'loose', 'strict'])->end() + ->enumNode('email_validation_mode')->values(Email::VALIDATION_MODES + ['loose'])->end() ->arrayNode('mapping') ->addDefaultsIfNotSet() ->fixXmlConfig('path') diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index 53268ffd2..eae457361 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase @@ -245,4 +246,31 @@ public function testRateLimiterLockFactory() $container->getDefinition('limiter.without_lock')->getArgument(2); } + + /** + * @dataProvider emailValidationModeProvider + */ + public function testValidatorEmailValidationMode(string $mode) + { + $this->expectNotToPerformAssertions(); + + $this->createContainerFromClosure(function (ContainerBuilder $container) use ($mode) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'validation' => [ + 'email_validation_mode' => $mode, + ], + ]); + }); + } + + public function emailValidationModeProvider() + { + foreach (Email::VALIDATION_MODES as $mode) { + yield [$mode]; + } + } } From b238ab0e1ab20793d151def4a4ef1da9d58362aa Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Thu, 8 May 2025 00:10:13 +0900 Subject: [PATCH 089/111] [FrameworkBundle] Ensure `Email` class exists before using it --- DependencyInjection/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 4d44c469f..bae8967a8 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1067,7 +1067,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e ->validate()->castToArray()->end() ->end() ->scalarNode('translation_domain')->defaultValue('validators')->end() - ->enumNode('email_validation_mode')->values(Email::VALIDATION_MODES + ['loose'])->end() + ->enumNode('email_validation_mode')->values((class_exists(Email::class) ? Email::VALIDATION_MODES : ['html5-allow-no-tld', 'html5', 'strict']) + ['loose'])->end() ->arrayNode('mapping') ->addDefaultsIfNotSet() ->fixXmlConfig('path') From b63dec25c8bb03e14e84fa878e306b44eb8810c9 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 8 May 2025 15:32:34 +0200 Subject: [PATCH 090/111] make data provider static --- Tests/DependencyInjection/PhpFrameworkExtensionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index eae457361..e5cc8522a 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -267,7 +267,7 @@ public function testValidatorEmailValidationMode(string $mode) }); } - public function emailValidationModeProvider() + public static function emailValidationModeProvider() { foreach (Email::VALIDATION_MODES as $mode) { yield [$mode]; From e35e3192856f4bdb232af5592c1a63daab3a2998 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 9 May 2025 10:05:11 +0200 Subject: [PATCH 091/111] [DependencyInjection][FrameworkBundle] Fix precedence of App\Kernel alias and ignore container.excluded tag on synthetic services --- DependencyInjection/FrameworkExtension.php | 20 ++++++++++---------- Kernel/MicroKernelTrait.php | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 2dd6ed95e..4b18b3817 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -791,34 +791,34 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu } $container->registerForAutoconfiguration(CompilerPassInterface::class) - ->addTag('container.excluded', ['source' => 'because it\'s a compiler pass'])->setAbstract(true); + ->addTag('container.excluded', ['source' => 'because it\'s a compiler pass']); $container->registerForAutoconfiguration(Constraint::class) - ->addTag('container.excluded', ['source' => 'because it\'s a validation constraint'])->setAbstract(true); + ->addTag('container.excluded', ['source' => 'because it\'s a validation constraint']); $container->registerForAutoconfiguration(TestCase::class) - ->addTag('container.excluded', ['source' => 'because it\'s a test case'])->setAbstract(true); + ->addTag('container.excluded', ['source' => 'because it\'s a test case']); $container->registerForAutoconfiguration(\UnitEnum::class) - ->addTag('container.excluded', ['source' => 'because it\'s an enum'])->setAbstract(true); + ->addTag('container.excluded', ['source' => 'because it\'s an enum']); $container->registerAttributeForAutoconfiguration(AsMessage::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded', ['source' => 'because it\'s a messenger message'])->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a messenger message']); }); $container->registerAttributeForAutoconfiguration(\Attribute::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded', ['source' => 'because it\'s an attribute'])->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a PHP attribute']); }); $container->registerAttributeForAutoconfiguration(Entity::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded', ['source' => 'because it\'s a doctrine entity'])->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a Doctrine entity']); }); $container->registerAttributeForAutoconfiguration(Embeddable::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded', ['source' => 'because it\'s a doctrine embeddable'])->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a Doctrine embeddable']); }); $container->registerAttributeForAutoconfiguration(MappedSuperclass::class, static function (ChildDefinition $definition) { - $definition->addTag('container.excluded', ['source' => 'because it\'s a doctrine mapped superclass'])->setAbstract(true); + $definition->addTag('container.excluded', ['source' => 'because it\'s a Doctrine mapped superclass']); }); $container->registerAttributeForAutoconfiguration(JsonStreamable::class, static function (ChildDefinition $definition, JsonStreamable $attribute) { $definition->addTag('json_streamer.streamable', [ 'object' => $attribute->asObject, 'list' => $attribute->asList, - ])->addTag('container.excluded', ['source' => 'because it\'s a streamable JSON'])->setAbstract(true); + ])->addTag('container.excluded', ['source' => 'because it\'s a streamable JSON']); }); if (!$container->getParameter('kernel.debug')) { diff --git a/Kernel/MicroKernelTrait.php b/Kernel/MicroKernelTrait.php index 28d616c13..f40373a30 100644 --- a/Kernel/MicroKernelTrait.php +++ b/Kernel/MicroKernelTrait.php @@ -165,7 +165,6 @@ public function registerContainerConfiguration(LoaderInterface $loader): void ->setPublic(true) ; } - $container->setAlias($kernelClass, 'kernel')->setPublic(true); $kernelDefinition = $container->getDefinition('kernel'); $kernelDefinition->addTag('routing.route_loader'); @@ -198,6 +197,8 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $kernelLoader->registerAliasesForSinglyImplementedInterfaces(); AbstractConfigurator::$valuePreProcessor = $valuePreProcessor; } + + $container->setAlias($kernelClass, 'kernel')->setPublic(true); }); } From 42cca5c8fb95d851bf3fb709217e8feb62a4376a Mon Sep 17 00:00:00 2001 From: Nowfel2501 Date: Fri, 9 May 2025 21:12:33 +0200 Subject: [PATCH 092/111] Improve readability of disallow_search_engine_index condition --- DependencyInjection/FrameworkExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index f585b5bbb..68386120e 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -735,7 +735,7 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu $container->getDefinition('config_cache_factory')->setArguments([]); } - if (!$config['disallow_search_engine_index'] ?? false) { + if (!$config['disallow_search_engine_index']) { $container->removeDefinition('disallow_search_engine_index_response_listener'); } From 87f0ace63cdbf4314fc223c193a375b86c23d4d1 Mon Sep 17 00:00:00 2001 From: Quentin Devos <4972091+Okhoshi@users.noreply.github.com> Date: Sat, 9 Dec 2023 21:15:58 +0100 Subject: [PATCH 093/111] [FrameworkBundle] Make `ValidatorCacheWarmer` and `SerializeCacheWarmer` use `kernel.build_dir` instead of `kernel.cache_dir` --- CHANGELOG.md | 2 + CacheWarmer/SerializerCacheWarmer.php | 3 + CacheWarmer/ValidatorCacheWarmer.php | 4 + Resources/config/serializer.php | 2 +- Resources/config/validator.php | 2 +- .../CacheWarmer/SerializerCacheWarmerTest.php | 74 ++++++++++++++--- .../CacheWarmer/ValidatorCacheWarmerTest.php | 81 ++++++++++++++++--- 7 files changed, 146 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e70fb98e..ec0d88fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ CHANGELOG * Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default * Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead * Allow configuring compound rate limiters + * Make `ValidatorCacheWarmer` use `kernel.build_dir` instead of `cache_dir` + * Make `SerializeCacheWarmer` use `kernel.build_dir` instead of `cache_dir` 7.2 --- diff --git a/CacheWarmer/SerializerCacheWarmer.php b/CacheWarmer/SerializerCacheWarmer.php index 46da4daaa..fbf7083b7 100644 --- a/CacheWarmer/SerializerCacheWarmer.php +++ b/CacheWarmer/SerializerCacheWarmer.php @@ -41,6 +41,9 @@ public function __construct( protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?string $buildDir = null): bool { + if (!$buildDir) { + return false; + } if (!$this->loaders) { return true; } diff --git a/CacheWarmer/ValidatorCacheWarmer.php b/CacheWarmer/ValidatorCacheWarmer.php index 6ecaa4bd1..9c313f80a 100644 --- a/CacheWarmer/ValidatorCacheWarmer.php +++ b/CacheWarmer/ValidatorCacheWarmer.php @@ -41,6 +41,10 @@ public function __construct( protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?string $buildDir = null): bool { + if (!$buildDir) { + return false; + } + $loaders = $this->validatorBuilder->getLoaders(); $metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), $arrayAdapter); diff --git a/Resources/config/serializer.php b/Resources/config/serializer.php index 535b95a39..e0a256bbe 100644 --- a/Resources/config/serializer.php +++ b/Resources/config/serializer.php @@ -56,7 +56,7 @@ return static function (ContainerConfigurator $container) { $container->parameters() - ->set('serializer.mapping.cache.file', '%kernel.cache_dir%/serialization.php') + ->set('serializer.mapping.cache.file', '%kernel.build_dir%/serialization.php') ; $container->services() diff --git a/Resources/config/validator.php b/Resources/config/validator.php index adde2de23..535b42edc 100644 --- a/Resources/config/validator.php +++ b/Resources/config/validator.php @@ -28,7 +28,7 @@ return static function (ContainerConfigurator $container) { $container->parameters() - ->set('validator.mapping.cache.file', param('kernel.cache_dir').'/validation.php'); + ->set('validator.mapping.cache.file', '%kernel.build_dir%/validation.php'); $validatorsDir = \dirname((new \ReflectionClass(EmailValidator::class))->getFileName()); diff --git a/Tests/CacheWarmer/SerializerCacheWarmerTest.php b/Tests/CacheWarmer/SerializerCacheWarmerTest.php index 5feb0c8ec..9b765c36a 100644 --- a/Tests/CacheWarmer/SerializerCacheWarmerTest.php +++ b/Tests/CacheWarmer/SerializerCacheWarmerTest.php @@ -30,9 +30,50 @@ public function testWarmUp(array $loaders) @unlink($file); $warmer = new SerializerCacheWarmer($loaders, $file); - $warmer->warmUp(\dirname($file)); + $warmer->warmUp(\dirname($file), \dirname($file)); + + $this->assertFileExists($file); + + $arrayPool = new PhpArrayAdapter($file, new NullAdapter()); + + $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Person')->isHit()); + $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author')->isHit()); + } + + /** + * @dataProvider loaderProvider + */ + public function testWarmUpAbsoluteFilePath(array $loaders) + { + $file = sys_get_temp_dir().'/0/cache-serializer.php'; + @unlink($file); + + $cacheDir = sys_get_temp_dir().'/1'; + + $warmer = new SerializerCacheWarmer($loaders, $file); + $warmer->warmUp($cacheDir, $cacheDir); $this->assertFileExists($file); + $this->assertFileDoesNotExist($cacheDir.'/cache-serializer.php'); + + $arrayPool = new PhpArrayAdapter($file, new NullAdapter()); + + $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Person')->isHit()); + $this->assertTrue($arrayPool->getItem('Symfony_Bundle_FrameworkBundle_Tests_Fixtures_Serialization_Author')->isHit()); + } + + /** + * @dataProvider loaderProvider + */ + public function testWarmUpWithoutBuildDir(array $loaders) + { + $file = sys_get_temp_dir().'/cache-serializer.php'; + @unlink($file); + + $warmer = new SerializerCacheWarmer($loaders, $file); + $warmer->warmUp(\dirname($file)); + + $this->assertFileDoesNotExist($file); $arrayPool = new PhpArrayAdapter($file, new NullAdapter()); @@ -66,7 +107,7 @@ public function testWarmUpWithoutLoader() @unlink($file); $warmer = new SerializerCacheWarmer([], $file); - $warmer->warmUp(\dirname($file)); + $warmer->warmUp(\dirname($file), \dirname($file)); $this->assertFileExists($file); } @@ -79,7 +120,10 @@ public function testClassAutoloadException() { $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest', false)); - $warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], tempnam(sys_get_temp_dir(), __FUNCTION__)); + $file = tempnam(sys_get_temp_dir(), __FUNCTION__); + @unlink($file); + + $warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], $file); spl_autoload_register($classLoader = function ($class) use ($mappedClass) { if ($class === $mappedClass) { @@ -87,7 +131,8 @@ public function testClassAutoloadException() } }, true, true); - $warmer->warmUp('foo'); + $warmer->warmUp(\dirname($file), \dirname($file)); + $this->assertFileExists($file); spl_autoload_unregister($classLoader); } @@ -98,12 +143,12 @@ public function testClassAutoloadException() */ public function testClassAutoloadExceptionWithUnrelatedException() { - $this->expectException(\DomainException::class); - $this->expectExceptionMessage('This exception should not be caught by the warmer.'); - $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest', false)); - $warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], tempnam(sys_get_temp_dir(), __FUNCTION__)); + $file = tempnam(sys_get_temp_dir(), __FUNCTION__); + @unlink($file); + + $warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], basename($file)); spl_autoload_register($classLoader = function ($class) use ($mappedClass) { if ($class === $mappedClass) { @@ -112,8 +157,17 @@ public function testClassAutoloadExceptionWithUnrelatedException() } }, true, true); - $warmer->warmUp('foo'); + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('This exception should not be caught by the warmer.'); + + try { + $warmer->warmUp(\dirname($file), \dirname($file)); + } catch (\DomainException $e) { + $this->assertFileDoesNotExist($file); - spl_autoload_unregister($classLoader); + throw $e; + } finally { + spl_autoload_unregister($classLoader); + } } } diff --git a/Tests/CacheWarmer/ValidatorCacheWarmerTest.php b/Tests/CacheWarmer/ValidatorCacheWarmerTest.php index cc471e43f..af0bb1b50 100644 --- a/Tests/CacheWarmer/ValidatorCacheWarmerTest.php +++ b/Tests/CacheWarmer/ValidatorCacheWarmerTest.php @@ -32,7 +32,7 @@ public function testWarmUp() @unlink($file); $warmer = new ValidatorCacheWarmer($validatorBuilder, $file); - $warmer->warmUp(\dirname($file)); + $warmer->warmUp(\dirname($file), \dirname($file)); $this->assertFileExists($file); @@ -42,6 +42,53 @@ public function testWarmUp() $this->assertTrue($arrayPool->getItem('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Author')->isHit()); } + public function testWarmUpAbsoluteFilePath() + { + $validatorBuilder = new ValidatorBuilder(); + $validatorBuilder->addXmlMapping(__DIR__.'/../Fixtures/Validation/Resources/person.xml'); + $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/author.yml'); + $validatorBuilder->addMethodMapping('loadValidatorMetadata'); + $validatorBuilder->enableAttributeMapping(); + + $file = sys_get_temp_dir().'/0/cache-validator.php'; + @unlink($file); + + $cacheDir = sys_get_temp_dir().'/1'; + + $warmer = new ValidatorCacheWarmer($validatorBuilder, $file); + $warmer->warmUp($cacheDir, $cacheDir); + + $this->assertFileExists($file); + $this->assertFileDoesNotExist($cacheDir.'/cache-validator.php'); + + $arrayPool = new PhpArrayAdapter($file, new NullAdapter()); + + $this->assertTrue($arrayPool->getItem('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Person')->isHit()); + $this->assertTrue($arrayPool->getItem('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Author')->isHit()); + } + + public function testWarmUpWithoutBuilDir() + { + $validatorBuilder = new ValidatorBuilder(); + $validatorBuilder->addXmlMapping(__DIR__.'/../Fixtures/Validation/Resources/person.xml'); + $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/author.yml'); + $validatorBuilder->addMethodMapping('loadValidatorMetadata'); + $validatorBuilder->enableAttributeMapping(); + + $file = sys_get_temp_dir().'/cache-validator.php'; + @unlink($file); + + $warmer = new ValidatorCacheWarmer($validatorBuilder, $file); + $warmer->warmUp(\dirname($file)); + + $this->assertFileDoesNotExist($file); + + $arrayPool = new PhpArrayAdapter($file, new NullAdapter()); + + $this->assertTrue($arrayPool->getItem('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Person')->isHit()); + $this->assertTrue($arrayPool->getItem('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Author')->isHit()); + } + public function testWarmUpWithAnnotations() { $validatorBuilder = new ValidatorBuilder(); @@ -52,7 +99,7 @@ public function testWarmUpWithAnnotations() @unlink($file); $warmer = new ValidatorCacheWarmer($validatorBuilder, $file); - $warmer->warmUp(\dirname($file)); + $warmer->warmUp(\dirname($file), \dirname($file)); $this->assertFileExists($file); @@ -72,7 +119,7 @@ public function testWarmUpWithoutLoader() @unlink($file); $warmer = new ValidatorCacheWarmer($validatorBuilder, $file); - $warmer->warmUp(\dirname($file)); + $warmer->warmUp(\dirname($file), \dirname($file)); $this->assertFileExists($file); } @@ -85,9 +132,12 @@ public function testClassAutoloadException() { $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest', false)); + $file = tempnam(sys_get_temp_dir(), __FUNCTION__); + @unlink($file); + $validatorBuilder = new ValidatorBuilder(); $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/does_not_exist.yaml'); - $warmer = new ValidatorCacheWarmer($validatorBuilder, tempnam(sys_get_temp_dir(), __FUNCTION__)); + $warmer = new ValidatorCacheWarmer($validatorBuilder, $file); spl_autoload_register($classloader = function ($class) use ($mappedClass) { if ($class === $mappedClass) { @@ -95,7 +145,9 @@ public function testClassAutoloadException() } }, true, true); - $warmer->warmUp('foo'); + $warmer->warmUp(\dirname($file), \dirname($file)); + + $this->assertFileExists($file); spl_autoload_unregister($classloader); } @@ -106,14 +158,14 @@ public function testClassAutoloadException() */ public function testClassAutoloadExceptionWithUnrelatedException() { - $this->expectException(\DomainException::class); - $this->expectExceptionMessage('This exception should not be caught by the warmer.'); + $file = tempnam(sys_get_temp_dir(), __FUNCTION__); + @unlink($file); $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest', false)); $validatorBuilder = new ValidatorBuilder(); $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/does_not_exist.yaml'); - $warmer = new ValidatorCacheWarmer($validatorBuilder, tempnam(sys_get_temp_dir(), __FUNCTION__)); + $warmer = new ValidatorCacheWarmer($validatorBuilder, basename($file)); spl_autoload_register($classLoader = function ($class) use ($mappedClass) { if ($class === $mappedClass) { @@ -122,8 +174,17 @@ public function testClassAutoloadExceptionWithUnrelatedException() } }, true, true); - $warmer->warmUp('foo'); + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('This exception should not be caught by the warmer.'); + + try { + $warmer->warmUp(\dirname($file), \dirname($file)); + } catch (\DomainException $e) { + $this->assertFileDoesNotExist($file); - spl_autoload_unregister($classLoader); + throw $e; + } finally { + spl_autoload_unregister($classLoader); + } } } From c5c63084286c21045144652b9dfd7d5b4ca5099d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 13 Mar 2024 18:34:52 +0100 Subject: [PATCH 094/111] [Workflow] Add support for executing custom workflow definition validators during the container compilation --- CHANGELOG.md | 1 + DependencyInjection/Configuration.php | 35 ++++++++--- DependencyInjection/FrameworkExtension.php | 35 ++++++----- FrameworkBundle.php | 2 + Resources/config/schema/symfony-1.0.xsd | 1 + .../Validator/DefinitionValidator.php | 16 +++++ .../Fixtures/php/workflows.php | 3 + .../Fixtures/xml/workflows.xml | 1 + .../Fixtures/yml/workflows.yml | 2 + .../FrameworkExtensionTestCase.php | 12 +++- .../PhpFrameworkExtensionTest.php | 61 ++++++++++++++++++- composer.json | 4 +- 12 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 Tests/DependencyInjection/Fixtures/Workflow/Validator/DefinitionValidator.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a3766d6..ce62c9cdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ CHANGELOG * Allow configuring compound rate limiters * Make `ValidatorCacheWarmer` use `kernel.build_dir` instead of `cache_dir` * Make `SerializeCacheWarmer` use `kernel.build_dir` instead of `cache_dir` + * Support executing custom workflow validators during container compilation 7.2 --- diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 11dc781ba..4c4045552 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -52,6 +52,7 @@ use Symfony\Component\Validator\Validation; use Symfony\Component\Webhook\Controller\WebhookController; use Symfony\Component\WebLink\HttpHeaderSerializer; +use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface; use Symfony\Component\Workflow\WorkflowEvents; /** @@ -403,6 +404,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->useAttributeAsKey('name') ->prototype('array') ->fixXmlConfig('support') + ->fixXmlConfig('definition_validator') ->fixXmlConfig('place') ->fixXmlConfig('transition') ->fixXmlConfig('event_to_dispatch', 'events_to_dispatch') @@ -432,11 +434,28 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->prototype('scalar') ->cannotBeEmpty() ->validate() - ->ifTrue(fn ($v) => !class_exists($v) && !interface_exists($v, false)) + ->ifTrue(static fn ($v) => !class_exists($v) && !interface_exists($v, false)) ->thenInvalid('The supported class or interface "%s" does not exist.') ->end() ->end() ->end() + ->arrayNode('definition_validators') + ->prototype('scalar') + ->cannotBeEmpty() + ->validate() + ->ifTrue(static fn ($v) => !class_exists($v)) + ->thenInvalid('The validation class %s does not exist.') + ->end() + ->validate() + ->ifTrue(static fn ($v) => !is_a($v, DefinitionValidatorInterface::class, true)) + ->thenInvalid(\sprintf('The validation class %%s is not an instance of "%s".', DefinitionValidatorInterface::class)) + ->end() + ->validate() + ->ifTrue(static fn ($v) => 1 <= (new \ReflectionClass($v))->getConstructor()?->getNumberOfRequiredParameters()) + ->thenInvalid('The %s validation class constructor must not have any arguments.') + ->end() + ->end() + ->end() ->scalarNode('support_strategy') ->cannotBeEmpty() ->end() @@ -448,7 +467,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->variableNode('events_to_dispatch') ->defaultValue(null) ->validate() - ->ifTrue(function ($v) { + ->ifTrue(static function ($v) { if (null === $v) { return false; } @@ -475,14 +494,14 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->arrayNode('places') ->beforeNormalization() ->always() - ->then(function ($places) { + ->then(static function ($places) { if (!\is_array($places)) { throw new InvalidConfigurationException('The "places" option must be an array in workflow configuration.'); } // It's an indexed array of shape ['place1', 'place2'] if (isset($places[0]) && \is_string($places[0])) { - return array_map(function (string $place) { + return array_map(static function (string $place) { return ['name' => $place]; }, $places); } @@ -522,7 +541,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->arrayNode('transitions') ->beforeNormalization() ->always() - ->then(function ($transitions) { + ->then(static function ($transitions) { if (!\is_array($transitions)) { throw new InvalidConfigurationException('The "transitions" option must be an array in workflow configuration.'); } @@ -589,20 +608,20 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->end() ->end() ->validate() - ->ifTrue(function ($v) { + ->ifTrue(static function ($v) { return $v['supports'] && isset($v['support_strategy']); }) ->thenInvalid('"supports" and "support_strategy" cannot be used together.') ->end() ->validate() - ->ifTrue(function ($v) { + ->ifTrue(static function ($v) { return !$v['supports'] && !isset($v['support_strategy']); }) ->thenInvalid('"supports" or "support_strategy" should be configured.') ->end() ->beforeNormalization() ->always() - ->then(function ($values) { + ->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'] = []; diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 4b18b3817..6df4d21df 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1123,7 +1123,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ } } $metadataStoreDefinition->replaceArgument(2, $transitionsMetadataDefinition); - $container->setDefinition(\sprintf('%s.metadata_store', $workflowId), $metadataStoreDefinition); + $metadataStoreId = \sprintf('%s.metadata_store', $workflowId); + $container->setDefinition($metadataStoreId, $metadataStoreDefinition); // Create places $places = array_column($workflow['places'], 'name'); @@ -1134,7 +1135,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $definitionDefinition->addArgument($places); $definitionDefinition->addArgument($transitions); $definitionDefinition->addArgument($initialMarking); - $definitionDefinition->addArgument(new Reference(\sprintf('%s.metadata_store', $workflowId))); + $definitionDefinition->addArgument(new Reference($metadataStoreId)); + $definitionDefinitionId = \sprintf('%s.definition', $workflowId); // Create MarkingStore $markingStoreDefinition = null; @@ -1148,14 +1150,26 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $markingStoreDefinition = new Reference($workflow['marking_store']['service']); } + // Validation + $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'])), + }; + // Create Workflow $workflowDefinition = new ChildDefinition(\sprintf('%s.abstract', $type)); - $workflowDefinition->replaceArgument(0, new Reference(\sprintf('%s.definition', $workflowId))); + $workflowDefinition->replaceArgument(0, new Reference($definitionDefinitionId)); $workflowDefinition->replaceArgument(1, $markingStoreDefinition); $workflowDefinition->replaceArgument(3, $name); $workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']); - $workflowDefinition->addTag('workflow', ['name' => $name, 'metadata' => $workflow['metadata']]); + $workflowDefinition->addTag('workflow', [ + 'name' => $name, + 'metadata' => $workflow['metadata'], + 'definition_validators' => $workflow['definition_validators'], + 'definition_id' => $definitionDefinitionId, + ]); if ('workflow' === $type) { $workflowDefinition->addTag('workflow.workflow', ['name' => $name]); } elseif ('state_machine' === $type) { @@ -1164,21 +1178,10 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Store to container $container->setDefinition($workflowId, $workflowDefinition); - $container->setDefinition(\sprintf('%s.definition', $workflowId), $definitionDefinition); + $container->setDefinition($definitionDefinitionId, $definitionDefinition); $container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type); $container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name); - // Validate Workflow - if ('state_machine' === $workflow['type']) { - $validator = new Workflow\Validator\StateMachineValidator(); - } else { - $validator = new Workflow\Validator\WorkflowValidator(); - } - - $trs = array_map(fn (Reference $ref): Workflow\Transition => $container->get((string) $ref), $transitions); - $realDefinition = new Workflow\Definition($places, $trs, $initialMarking); - $validator->validate($realDefinition, $name); - // Add workflow to Registry if ($workflow['supports']) { foreach ($workflow['supports'] as $supportedClassName) { diff --git a/FrameworkBundle.php b/FrameworkBundle.php index faf2841f4..7c5ba6e39 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -77,6 +77,7 @@ use Symfony\Component\VarExporter\Internal\Registry; use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass; use Symfony\Component\Workflow\DependencyInjection\WorkflowGuardListenerPass; +use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; // Help opcache.preload discover always-needed symbols class_exists(ApcuAdapter::class); @@ -173,6 +174,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING); $this->addCompilerPassIfExists($container, FormPass::class); $this->addCompilerPassIfExists($container, WorkflowGuardListenerPass::class); + $this->addCompilerPassIfExists($container, WorkflowValidatorPass::class); $container->addCompilerPass(new ResettableServicePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterLocaleAwareServicesPass()); $container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32); diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index c4ee3486d..3a6242b83 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -449,6 +449,7 @@ + diff --git a/Tests/DependencyInjection/Fixtures/Workflow/Validator/DefinitionValidator.php b/Tests/DependencyInjection/Fixtures/Workflow/Validator/DefinitionValidator.php new file mode 100644 index 000000000..7244e927c --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/Workflow/Validator/DefinitionValidator.php @@ -0,0 +1,16 @@ + [ FrameworkExtensionTestCase::class, ], + 'definition_validators' => [ + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator::class, + ], 'initial_marking' => ['draft'], 'metadata' => [ 'title' => 'article workflow', diff --git a/Tests/DependencyInjection/Fixtures/xml/workflows.xml b/Tests/DependencyInjection/Fixtures/xml/workflows.xml index 76b4f07a8..c5dae479d 100644 --- a/Tests/DependencyInjection/Fixtures/xml/workflows.xml +++ b/Tests/DependencyInjection/Fixtures/xml/workflows.xml @@ -13,6 +13,7 @@ draft Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator diff --git a/Tests/DependencyInjection/Fixtures/yml/workflows.yml b/Tests/DependencyInjection/Fixtures/yml/workflows.yml index a9b427d89..cac5f6f23 100644 --- a/Tests/DependencyInjection/Fixtures/yml/workflows.yml +++ b/Tests/DependencyInjection/Fixtures/yml/workflows.yml @@ -9,6 +9,8 @@ framework: type: workflow supports: - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTestCase + definition_validators: + - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator initial_marking: [draft] metadata: title: article workflow diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index d942c122c..1899d5239 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -15,6 +15,7 @@ use Psr\Log\LoggerAwareInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\Workflow\Validator\DefinitionValidator; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FullStack; @@ -287,7 +288,11 @@ public function testProfilerCollectSerializerDataEnabled() public function testWorkflows() { - $container = $this->createContainerFromFile('workflows'); + DefinitionValidator::$called = false; + + $container = $this->createContainerFromFile('workflows', compile: false); + $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->compile(); $this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service'); $this->assertSame('workflow.abstract', $container->getDefinition('workflow.article')->getParent()); @@ -310,6 +315,7 @@ public function testWorkflows() ], $tags['workflow'][0]['metadata'] ?? null); $this->assertTrue($container->hasDefinition('workflow.article.definition'), 'Workflow definition is registered as a service'); + $this->assertTrue(DefinitionValidator::$called, 'DefinitionValidator is called'); $workflowDefinition = $container->getDefinition('workflow.article.definition'); @@ -403,7 +409,9 @@ public function testWorkflowAreValidated() { $this->expectException(InvalidDefinitionException::class); $this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" were found on StateMachine "my_workflow".'); - $this->createContainerFromFile('workflow_not_valid'); + $container = $this->createContainerFromFile('workflow_not_valid', compile: false); + $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->compile(); } public function testWorkflowCannotHaveBothSupportsAndSupportStrategy() diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index dbadcc468..d2bd2b38e 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -102,7 +102,7 @@ public function testWorkflowValidationStateMachine() { $this->expectException(InvalidDefinitionException::class); $this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "a_to_b" from place/state "a" were found on StateMachine "article".'); - $this->createContainerFromClosure(function ($container) { + $this->createContainerFromClosure(function (ContainerBuilder $container) { $container->loadFromExtension('framework', [ 'annotations' => false, 'http_method_override' => false, @@ -128,9 +128,57 @@ public function testWorkflowValidationStateMachine() ], ], ]); + $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + }); + } + + /** + * @dataProvider provideWorkflowValidationCustomTests + */ + public function testWorkflowValidationCustomBroken(string $class, string $message) + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage($message); + $this->createContainerFromClosure(function ($container) use ($class) { + $container->loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'workflows' => [ + 'article' => [ + 'type' => 'state_machine', + 'supports' => [ + __CLASS__, + ], + 'places' => [ + 'a', + 'b', + ], + 'transitions' => [ + 'a_to_b' => [ + 'from' => ['a'], + 'to' => ['b'], + ], + ], + 'definition_validators' => [ + $class, + ], + ], + ], + ]); }); } + public static function provideWorkflowValidationCustomTests() + { + yield ['classDoesNotExist', 'Invalid configuration for path "framework.workflows.workflows.article.definition_validators.0": The validation class "classDoesNotExist" does not exist.']; + + yield [\DateTime::class, 'Invalid configuration for path "framework.workflows.workflows.article.definition_validators.0": The validation class "DateTime" is not an instance of "Symfony\Component\Workflow\Validator\DefinitionValidatorInterface".']; + + yield [WorkflowValidatorWithConstructor::class, 'Invalid configuration for path "framework.workflows.workflows.article.definition_validators.0": The "Symfony\\\\Bundle\\\\FrameworkBundle\\\\Tests\\\\DependencyInjection\\\\WorkflowValidatorWithConstructor" validation class constructor must not have any arguments.']; + } + public function testWorkflowDefaultMarkingStoreDefinition() { $container = $this->createContainerFromClosure(function ($container) { @@ -407,3 +455,14 @@ public static function emailValidationModeProvider() } } } + +class WorkflowValidatorWithConstructor implements \Symfony\Component\Workflow\Validator\DefinitionValidatorInterface +{ + public function __construct(bool $enabled) + { + } + + public function validate(\Symfony\Component\Workflow\Definition $definition, string $name): void + { + } +} diff --git a/composer.json b/composer.json index bc312827f..b3c81b287 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ "symfony/twig-bundle": "^6.4|^7.0", "symfony/type-info": "^7.1", "symfony/validator": "^6.4|^7.0", - "symfony/workflow": "^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.*", @@ -108,7 +108,7 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<6.4" + "symfony/workflow": "<7.3" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, From b096abaa51e1939c01300a6defc7cf24b2539fee Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 12 May 2025 09:26:05 +0200 Subject: [PATCH 095/111] fix lowest allowed Workflow component version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b3c81b287..316c595ff 100644 --- a/composer.json +++ b/composer.json @@ -108,7 +108,7 @@ "symfony/validator": "<6.4", "symfony/web-profiler-bundle": "<6.4", "symfony/webhook": "<7.2", - "symfony/workflow": "<7.3" + "symfony/workflow": "<7.3.0-beta2" }, "autoload": { "psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" }, From 8ac582e68a0431df291e330e4892bc667518d8a6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 12 May 2025 10:58:22 +0200 Subject: [PATCH 096/111] use use statements instead of FQCNs --- Tests/DependencyInjection/FrameworkExtensionTestCase.php | 5 +++-- Tests/DependencyInjection/PhpFrameworkExtensionTest.php | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 1899d5239..990e1e8c2 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -92,6 +92,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Webhook\Client\RequestParser; use Symfony\Component\Webhook\Controller\WebhookController; +use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\WorkflowEvents; @@ -291,7 +292,7 @@ public function testWorkflows() DefinitionValidator::$called = false; $container = $this->createContainerFromFile('workflows', compile: false); - $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->addCompilerPass(new WorkflowValidatorPass()); $container->compile(); $this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service'); @@ -410,7 +411,7 @@ public function testWorkflowAreValidated() $this->expectException(InvalidDefinitionException::class); $this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" were found on StateMachine "my_workflow".'); $container = $this->createContainerFromFile('workflow_not_valid', compile: false); - $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->addCompilerPass(new WorkflowValidatorPass()); $container->compile(); } diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index d2bd2b38e..f69a53932 100644 --- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -20,7 +20,10 @@ use Symfony\Component\RateLimiter\CompoundRateLimiterFactory; use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; use Symfony\Component\Validator\Constraints\Email; +use Symfony\Component\Workflow\Definition; +use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; use Symfony\Component\Workflow\Exception\InvalidDefinitionException; +use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface; class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase { @@ -128,7 +131,7 @@ public function testWorkflowValidationStateMachine() ], ], ]); - $container->addCompilerPass(new \Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass()); + $container->addCompilerPass(new WorkflowValidatorPass()); }); } @@ -456,13 +459,13 @@ public static function emailValidationModeProvider() } } -class WorkflowValidatorWithConstructor implements \Symfony\Component\Workflow\Validator\DefinitionValidatorInterface +class WorkflowValidatorWithConstructor implements DefinitionValidatorInterface { public function __construct(bool $enabled) { } - public function validate(\Symfony\Component\Workflow\Definition $definition, string $name): void + public function validate(Definition $definition, string $name): void { } } From b1de19b2083484d0ce945977f6c6484e9e493a2e Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 14 May 2025 09:14:36 +0200 Subject: [PATCH 097/111] remove no longer used service definition --- Resources/config/mailer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Resources/config/mailer.php b/Resources/config/mailer.php index 7a3a95739..f1dc560ab 100644 --- a/Resources/config/mailer.php +++ b/Resources/config/mailer.php @@ -45,7 +45,6 @@ tagged_iterator('mailer.transport_factory'), ]) - ->set('mailer.default_transport', TransportInterface::class) ->alias('mailer.default_transport', 'mailer.transports') ->alias(TransportInterface::class, 'mailer.default_transport') From 639c32f8e2314173a535cfeab683643af3a759b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 2 May 2025 14:12:20 +0200 Subject: [PATCH 098/111] [FrameworkBundle] skip messenger deduplication middlerware registration when no "default" lock is configured --- DependencyInjection/FrameworkExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 385a4caf3..fce6ba65c 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -569,9 +569,9 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('console.command.scheduler_debug'); } - // messenger depends on validation being registered + // messenger depends on validation, and lock being registered if ($messengerEnabled) { - $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $this->readConfigEnabled('validation', $container, $config['validation']), $this->readConfigEnabled('lock', $container, $config['lock'])); + $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $this->readConfigEnabled('validation', $container, $config['validation']), $this->readConfigEnabled('lock', $container, $config['lock']) && ($config['lock']['resources']['default'] ?? false)); } else { $container->removeDefinition('console.command.messenger_consume_messages'); $container->removeDefinition('console.command.messenger_stats'); From 7627f005de900e19fde7199247fba325690b0407 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 16 May 2025 15:57:08 +0200 Subject: [PATCH 099/111] [FrameworkBundle] Fix declaring fiel-attr tags in xml config files --- DependencyInjection/Configuration.php | 1 + Resources/config/schema/symfony-1.0.xsd | 2 +- .../Fixtures/php/form_csrf_field_attr.php | 22 +++++++++++++++++++ ...ield_name.xml => form_csrf_field_attr.xml} | 8 ++++++- .../form_csrf_under_form_sets_field_name.xml | 15 ------------- .../Fixtures/yml/form_csrf_field_attr.yml | 16 ++++++++++++++ .../FrameworkExtensionTestCase.php | 11 ++++++++++ 7 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php rename Tests/DependencyInjection/Fixtures/xml/{form_csrf_sets_field_name.xml => form_csrf_field_attr.xml} (67%) delete mode 100644 Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml create mode 100644 Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 50c093f28..5154393f2 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -252,6 +252,7 @@ private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableI ->arrayNode('field_attr') ->performNoDeepMerging() ->normalizeKeys(false) + ->useAttributeAsKey('name') ->scalarPrototype()->end() ->defaultValue(['data-controller' => 'csrf-protection']) ->end() diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd index 491cd1e4f..e99022acf 100644 --- a/Resources/config/schema/symfony-1.0.xsd +++ b/Resources/config/schema/symfony-1.0.xsd @@ -79,7 +79,7 @@ - + diff --git a/Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php b/Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php new file mode 100644 index 000000000..103ee4797 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/form_csrf_field_attr.php @@ -0,0 +1,22 @@ +loadFromExtension('framework', [ + 'annotations' => false, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'csrf_protection' => [ + 'enabled' => true, + ], + 'form' => [ + 'csrf_protection' => [ + 'field-attr' => [ + 'data-foo' => 'bar', + 'data-bar' => 'baz', + ], + ], + ], + 'session' => [ + 'storage_factory_id' => 'session.storage.factory.native', + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml b/Tests/DependencyInjection/Fixtures/xml/form_csrf_field_attr.xml similarity index 67% rename from Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml rename to Tests/DependencyInjection/Fixtures/xml/form_csrf_field_attr.xml index 4a05e9d33..1889703be 100644 --- a/Tests/DependencyInjection/Fixtures/xml/form_csrf_sets_field_name.xml +++ b/Tests/DependencyInjection/Fixtures/xml/form_csrf_field_attr.xml @@ -9,7 +9,13 @@ - + + + + bar + baz + + diff --git a/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml b/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml deleted file mode 100644 index 09ef0ee16..000000000 --- a/Tests/DependencyInjection/Fixtures/xml/form_csrf_under_form_sets_field_name.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - diff --git a/Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml b/Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml new file mode 100644 index 000000000..db5199775 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/form_csrf_field_attr.yml @@ -0,0 +1,16 @@ +framework: + annotations: false + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + csrf_protection: + enabled: true + form: + csrf_protection: + enabled: true + field_attr: + data-foo: bar + data-bar: baz + session: + storage_factory_id: session.storage.factory.native diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 7bf66512d..655f9180e 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1413,6 +1413,17 @@ public function testFormsCanBeEnabledWithoutCsrfProtection() $this->assertFalse($container->getParameter('form.type_extension.csrf.enabled')); } + public function testFormCsrfFieldAttr() + { + $container = $this->createContainerFromFile('form_csrf_field_attr'); + + $expected = [ + 'data-foo' => 'bar', + 'data-bar' => 'baz', + ]; + $this->assertSame($expected, $container->getParameter('form.type_extension.csrf.field_attr')); + } + public function testStopwatchEnabledWithDebugModeEnabled() { $container = $this->createContainerFromFile('default_config', [ From f5158cf658e912609fa443c77bfd37a4b0ea67d9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 20 May 2025 10:48:43 +0200 Subject: [PATCH 100/111] [FrameworkBundle] Fix activation strategy of traceable decorators --- FrameworkBundle.php | 9 +++++++++ Resources/config/profiling.php | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 7c5ba6e39..300fe22fb 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass; @@ -202,6 +203,14 @@ public function build(ContainerBuilder $container): void } } + /** + * @internal + */ + public static function considerProfilerEnabled(): bool + { + return !($GLOBALS['app'] ?? null) instanceof Application || empty($_GET) && \in_array('--profile', $_SERVER['argv'] ?? [], true); + } + private function addCompilerPassIfExists(ContainerBuilder $container, string $class, string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0): void { $container->addResource(new ClassExistenceResource($class)); diff --git a/Resources/config/profiling.php b/Resources/config/profiling.php index 68fb295bb..a81c53a63 100644 --- a/Resources/config/profiling.php +++ b/Resources/config/profiling.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FrameworkBundle\EventListener\ConsoleProfilerListener; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Component\HttpKernel\Debug\VirtualRequestStack; use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; @@ -61,7 +62,7 @@ ->set('profiler.state_checker', ProfilerStateChecker::class) ->args([ service_locator(['profiler' => service('profiler')->ignoreOnUninitialized()]), - param('kernel.runtime_mode.web'), + inline_service('bool')->factory([FrameworkBundle::class, 'considerProfilerEnabled']), ]) ->set('profiler.is_disabled_state_checker', 'Closure') From 95c9a2cdf5d04816ffdff1378ecd53d0f3a139aa Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 20 May 2025 13:53:54 +0200 Subject: [PATCH 101/111] [FrameworkBundle] object mapper service definition without form --- DependencyInjection/FrameworkExtension.php | 16 ++++++++-------- .../FrameworkExtensionTestCase.php | 8 ++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 385a4caf3..912282f49 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -642,6 +642,14 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('mime_type.php'); } + if (ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) { + $loader->load('object_mapper.php'); + $container->registerForAutoconfiguration(TransformCallableInterface::class) + ->addTag('object_mapper.transform_callable'); + $container->registerForAutoconfiguration(ConditionCallableInterface::class) + ->addTag('object_mapper.condition_callable'); + } + $container->registerForAutoconfiguration(PackageInterface::class) ->addTag('assets.package'); $container->registerForAutoconfiguration(AssetCompilerInterface::class) @@ -880,14 +888,6 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) { $container->removeDefinition('form.type_extension.upload.validator'); } - - if (ContainerBuilder::willBeAvailable('symfony/object-mapper', ObjectMapperInterface::class, ['symfony/framework-bundle'])) { - $loader->load('object_mapper.php'); - $container->registerForAutoconfiguration(TransformCallableInterface::class) - ->addTag('object_mapper.transform_callable'); - $container->registerForAutoconfiguration(ConditionCallableInterface::class) - ->addTag('object_mapper.condition_callable'); - } } private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 990e1e8c2..8b452d4c8 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -2600,6 +2600,14 @@ public function testJsonStreamerEnabled() $this->assertTrue($container->has('json_streamer.stream_writer')); } + public function testObjectMapperEnabled() + { + $container = $this->createContainerFromClosure(function (ContainerBuilder $container) { + $container->loadFromExtension('framework', []); + }); + $this->assertTrue($container->has('object_mapper')); + } + protected function createContainer(array $data = []) { return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ From 3614e216d77431bf0bb68e03299fb7303fd2980e Mon Sep 17 00:00:00 2001 From: HypeMC Date: Wed, 21 May 2025 14:18:26 +0200 Subject: [PATCH 102/111] [PropertyInfo] Improve deprecation message --- DependencyInjection/Configuration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 0e9b20b0c..f4e137f04 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1290,7 +1290,7 @@ private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable ->then(function ($v) { $v['property_info']['with_constructor_extractor'] = false; - trigger_deprecation('symfony/framework-bundle', '7.3', 'Not setting the "with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); + trigger_deprecation('symfony/framework-bundle', '7.3', 'Not setting the "property_info.with_constructor_extractor" option explicitly is deprecated because its default value will change in version 8.0.'); return $v; }) From 8122caa52c3b45a0a9e30168834757596bffb87d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 24 May 2025 23:27:21 +0200 Subject: [PATCH 103/111] conflict with 7.4 releases of experimental components --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 316c595ff..15a9496d1 100644 --- a/composer.json +++ b/composer.json @@ -94,6 +94,7 @@ "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 d35386ab398be41822b464b1027488c5b8b39f14 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sun, 25 May 2025 23:10:07 +0200 Subject: [PATCH 104/111] [Webhook] Fix controller service name --- Resources/config/routing/webhook.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/config/routing/webhook.php b/Resources/config/routing/webhook.php index ea8031159..177606b26 100644 --- a/Resources/config/routing/webhook.php +++ b/Resources/config/routing/webhook.php @@ -24,7 +24,7 @@ } $routes->add('_webhook_controller', '/{type}') - ->controller('webhook_controller::handle') + ->controller('webhook.controller::handle') ->requirements(['type' => '.+']) ; }; From 030646f55fe18501a43edab22a8ad250d8ec42a6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 27 May 2025 10:30:45 +0200 Subject: [PATCH 105/111] disable the Lock integration to not register the deduplicate middleware --- .../messenger_multiple_buses_without_deduplicate_middleware.php | 1 + .../messenger_multiple_buses_without_deduplicate_middleware.xml | 1 + .../messenger_multiple_buses_without_deduplicate_middleware.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php index b8e7530bb..fd4a00834 100644 --- a/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php +++ b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php @@ -5,6 +5,7 @@ 'http_method_override' => false, 'handle_all_throwables' => true, 'php_errors' => ['log' => true], + 'lock' => false, 'messenger' => [ 'default_bus' => 'messenger.bus.commands', 'buses' => [ diff --git a/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml index dcf402e1a..3f0d96249 100644 --- a/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml +++ b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml @@ -8,6 +8,7 @@ + diff --git a/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml index f06d534a5..38fca5737 100644 --- a/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml +++ b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml @@ -4,6 +4,7 @@ framework: handle_all_throwables: true php_errors: log: true + lock: false messenger: default_bus: messenger.bus.commands buses: From 9df8e51501cce65c7b259f4644663aa0476c9501 Mon Sep 17 00:00:00 2001 From: matlec Date: Fri, 30 May 2025 13:06:20 +0200 Subject: [PATCH 106/111] Fix `ContainerDebugCommandTest::testNoDumpedXML` --- Tests/Functional/ContainerDebugCommandTest.php | 2 +- Tests/Functional/app/ContainerDebug/no_dump.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Tests/Functional/app/ContainerDebug/no_dump.yml diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php index 24c6faf33..291a67cb8 100644 --- a/Tests/Functional/ContainerDebugCommandTest.php +++ b/Tests/Functional/ContainerDebugCommandTest.php @@ -53,7 +53,7 @@ public function testNoDebug() public function testNoDumpedXML() { - static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml', 'debug' => true, 'debug.container.dump' => false]); + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'no_dump.yml', 'debug' => true]); $application = new Application(static::$kernel); $application->setAutoExit(false); diff --git a/Tests/Functional/app/ContainerDebug/no_dump.yml b/Tests/Functional/app/ContainerDebug/no_dump.yml new file mode 100644 index 000000000..a9c709e9a --- /dev/null +++ b/Tests/Functional/app/ContainerDebug/no_dump.yml @@ -0,0 +1,5 @@ +imports: + - { resource: config.yml } + +parameters: + debug.container.dump: false From 9b1edd26071775162c2df241634496fbbe683399 Mon Sep 17 00:00:00 2001 From: Indra Gunawan Date: Mon, 2 Jun 2025 16:16:20 +0800 Subject: [PATCH 107/111] [FrameworkBundle] set NamespacedPoolInterface alias to cache.app --- DependencyInjection/FrameworkExtension.php | 4 ++++ Resources/config/cache.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 347f3ed65..64d27bca9 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -302,6 +302,10 @@ public function load(array $configs, ContainerBuilder $container): void // Load Cache configuration first as it is used by other components $loader->load('cache.php'); + if (!interface_exists(NamespacedPoolInterface::class)) { + $container->removeAlias(NamespacedPoolInterface::class); + } + $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); diff --git a/Resources/config/cache.php b/Resources/config/cache.php index 3d96ba059..ae9d426a4 100644 --- a/Resources/config/cache.php +++ b/Resources/config/cache.php @@ -28,6 +28,7 @@ use Symfony\Component\Cache\Messenger\EarlyExpirationHandler; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\NamespacedPoolInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; return static function (ContainerConfigurator $container) { @@ -250,6 +251,8 @@ ->alias(CacheInterface::class, 'cache.app') + ->alias(NamespacedPoolInterface::class, 'cache.app') + ->alias(TagAwareCacheInterface::class, 'cache.app.taggable') ; }; From 5df58fd260e6f0902f2719803818c689972e175b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 3 Jun 2025 08:30:01 +0200 Subject: [PATCH 108/111] don't register SchedulerTriggerNormalizer without symfony/serializer --- DependencyInjection/FrameworkExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index 64d27bca9..05504af2e 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -2297,7 +2297,7 @@ private function registerSchedulerConfiguration(ContainerBuilder $container, Php } // BC layer Scheduler < 7.3 - if (!class_exists(SchedulerTriggerNormalizer::class)) { + if (!ContainerBuilder::willBeAvailable('symfony/serializer', DenormalizerInterface::class, ['symfony/framework-bundle', 'symfony/scheduler']) || !class_exists(SchedulerTriggerNormalizer::class)) { $container->removeDefinition('serializer.normalizer.scheduler_trigger'); } } From 35a2a545f553c30c4679d3797456f82f3d77e08c Mon Sep 17 00:00:00 2001 From: Carlos Quintana Date: Tue, 27 May 2025 14:57:57 +0200 Subject: [PATCH 109/111] [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass --- Test/KernelTestCase.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Test/KernelTestCase.php b/Test/KernelTestCase.php index ee67fa7af..e87ac4824 100644 --- a/Test/KernelTestCase.php +++ b/Test/KernelTestCase.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Test; +use PHPUnit\Framework\Attributes\AfterClass; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -120,8 +121,11 @@ protected static function createKernel(array $options = []): KernelInterface /** * Shuts the kernel down if it was used in the test - called by the tearDown method by default. + * + * @afterClass */ - protected static function ensureKernelShutdown() + #[AfterClass] + public static function ensureKernelShutdown() { if (null !== static::$kernel) { static::$kernel->boot(); From fe8367b6b3593e5451e419c00afd7e780cfce647 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 4 Jun 2025 15:57:42 +0200 Subject: [PATCH 110/111] Revert "bug #60564 [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass (cquintana92)" This reverts commit ec76ab4f28454ebfbcf14287b2aac1351f00df79, reversing changes made to bc886008906f022a8fbf9796b943af928b64d86c. --- Test/KernelTestCase.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Test/KernelTestCase.php b/Test/KernelTestCase.php index e87ac4824..ee67fa7af 100644 --- a/Test/KernelTestCase.php +++ b/Test/KernelTestCase.php @@ -11,7 +11,6 @@ namespace Symfony\Bundle\FrameworkBundle\Test; -use PHPUnit\Framework\Attributes\AfterClass; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -121,11 +120,8 @@ protected static function createKernel(array $options = []): KernelInterface /** * Shuts the kernel down if it was used in the test - called by the tearDown method by default. - * - * @afterClass */ - #[AfterClass] - public static function ensureKernelShutdown() + protected static function ensureKernelShutdown() { if (null !== static::$kernel) { static::$kernel->boot(); From 26ca2357cab5dfe862f2bbf397bd1459a8901af8 Mon Sep 17 00:00:00 2001 From: Carlos Quintana Date: Tue, 27 May 2025 14:57:57 +0200 Subject: [PATCH 111/111] [FrameworkBundle] ensureKernelShutdown in tearDownAfterClass --- Test/KernelTestCase.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Test/KernelTestCase.php b/Test/KernelTestCase.php index ee67fa7af..1312f6592 100644 --- a/Test/KernelTestCase.php +++ b/Test/KernelTestCase.php @@ -45,6 +45,14 @@ protected function tearDown(): void static::$booted = false; } + public static function tearDownAfterClass(): void + { + static::ensureKernelShutdown(); + static::$class = null; + static::$kernel = null; + static::$booted = false; + } + /** * @throws \RuntimeException * @throws \LogicException