diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3227eddc2..ce62c9cdf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,62 @@
CHANGELOG
=========
+7.3
+---
+
+ * 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`
+ * 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 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
+ * 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
+ * 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`
+ * 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`
+ * Support executing custom workflow validators during container compilation
+
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/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/Command/ContainerDebugCommand.php b/Command/ContainerDebugCommand.php
index 2c5b8291b..17c71bdca 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/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/Command/TranslationExtractCommand.php b/Command/TranslationExtractCommand.php
new file mode 100644
index 000000000..c8e61b61a
--- /dev/null
+++ b/Command/TranslationExtractCommand.php
@@ -0,0 +1,503 @@
+
+ *
+ * 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();
+
+ if (!method_exists($writer, 'getFormats')) {
+ throw new \InvalidArgumentException(\sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class));
+ }
+ }
+
+ 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_REQUIRED, '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_REQUIRED, '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_REQUIRED, 'Specify the domain to extract'),
+ new InputOption('sort', null, InputOption::VALUE_REQUIRED, 'Return list of messages sorted alphabetically'),
+ new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
+ ])
+ ->setHelp(<<<'EOF'
+The %command.name% command extracts translation strings from templates
+of a given bundle or the default translations directory. It can display them or merge
+the new ones into the translation files.
+
+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 ed59d7eed..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,445 +28,7 @@ public function __construct(
private array $codePaths = [],
private array $enabledLocales = [],
) {
- parent::__construct();
-
- if (!method_exists($writer, 'getFormats')) {
- throw new \InvalidArgumentException(\sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class));
- }
- }
-
- 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_REQUIRED, '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_REQUIRED, '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_REQUIRED, 'Specify the domain to extract'),
- new InputOption('sort', null, InputOption::VALUE_REQUIRED, 'Return list of messages sorted alphabetically'),
- new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML'),
- ])
- ->setHelp(<<<'EOF'
-The %command.name% command extracts translation strings from templates
-of a given bundle or the default translations directory. It can display them or merge
-the new ones into the translation files.
-
-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/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/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/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/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php
index ae2523e51..53361e312 100644
--- a/DependencyInjection/Compiler/UnusedTagsPass.php
+++ b/DependencyInjection/Compiler/UnusedTagsPass.php
@@ -53,6 +53,7 @@ class UnusedTagsPass implements CompilerPassInterface
'form.type_guesser',
'html_sanitizer',
'http_client.client',
+ 'json_streamer.value_transformer',
'kernel.cache_clearer',
'kernel.cache_warmer',
'kernel.event_listener',
@@ -70,6 +71,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',
@@ -82,6 +84,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',
@@ -103,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/Configuration.php b/DependencyInjection/Configuration.php
index f6b40030b..d042d44b5 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;
@@ -30,6 +31,7 @@
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\IpUtils;
+use Symfony\Component\JsonStreamer\StreamWriterInterface;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
@@ -50,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;
/**
@@ -74,6 +77,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) {
@@ -181,6 +185,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$this->addHtmlSanitizerSection($rootNode, $enableIfStandalone);
$this->addWebhookSection($rootNode, $enableIfStandalone);
$this->addRemoteEventSection($rootNode, $enableIfStandalone);
+ $this->addJsonStreamerSection($rootNode, $enableIfStandalone);
return $treeBuilder;
}
@@ -400,6 +405,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')
@@ -429,11 +435,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()
@@ -445,7 +468,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;
}
@@ -472,14 +495,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);
}
@@ -519,7 +542,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.');
}
@@ -586,20 +609,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'] = [];
@@ -927,6 +950,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()
@@ -943,6 +989,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".')
@@ -997,6 +1044,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()
@@ -1011,7 +1085,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'])
@@ -1031,18 +1107,17 @@ 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.')
->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([
@@ -1203,8 +1278,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/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;
+ })
+ ->end()
;
}
@@ -1245,6 +1335,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()
@@ -1389,6 +1480,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()
@@ -1535,6 +1630,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.')
@@ -1667,7 +1763,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')
@@ -2202,6 +2317,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('repository')
+ ->info('S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`.')
+ ->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()
@@ -2329,7 +2526,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.')
@@ -2342,7 +2539,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.')
@@ -2361,8 +2563,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()
@@ -2549,4 +2751,16 @@ private function addHtmlSanitizerSection(ArrayNodeDefinition $rootNode, callable
->end()
;
}
+
+ private function addJsonStreamerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone): void
+ {
+ $rootNode
+ ->children()
+ ->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 c9d138fa5..0cf63bfea 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;
@@ -32,6 +36,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;
@@ -48,13 +53,17 @@
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;
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\Attribute\Target;
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;
@@ -98,20 +107,32 @@
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;
+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;
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\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;
+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;
@@ -127,8 +148,12 @@
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;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
@@ -136,18 +161,20 @@
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\CompoundRateLimiterFactory;
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;
+use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Scheduler\Attribute\AsCronTask;
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;
@@ -163,6 +190,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;
@@ -173,12 +201,15 @@
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;
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\Constraint;
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\GroupProviderInterface;
@@ -193,6 +224,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;
@@ -271,6 +303,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);
@@ -395,7 +431,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)) {
@@ -415,12 +464,20 @@ 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);
}
if ($propertyInfoEnabled) {
- $this->registerPropertyInfoConfiguration($container, $loader);
+ $this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader);
+ }
+
+ if ($this->readConfigEnabled('json_streamer', $container, $config['json_streamer'])) {
+ if (!$typeInfoEnabled) {
+ 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->registerJsonStreamerConfiguration($config['json_streamer'], $container, $loader);
}
if ($this->readConfigEnabled('lock', $container, $config['lock'])) {
@@ -517,9 +574,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->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');
@@ -590,10 +647,25 @@ 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)
->addTag('asset_mapper.compiler');
+ $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute): void {
+ $definition->addTag('console.command', [
+ 'command' => $attribute->name,
+ 'description' => $attribute->description,
+ 'help' => $attribute->help ?? null,
+ ]);
+ });
$container->registerForAutoconfiguration(Command::class)
->addTag('console.command');
$container->registerForAutoconfiguration(ResourceCheckerInterface::class)
@@ -642,6 +714,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)
@@ -684,6 +758,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]);
});
@@ -726,6 +803,37 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu
);
}
+ $container->registerForAutoconfiguration(CompilerPassInterface::class)
+ ->addTag('container.excluded', ['source' => 'because it\'s a compiler pass']);
+ $container->registerForAutoconfiguration(Constraint::class)
+ ->addTag('container.excluded', ['source' => 'because it\'s a validation constraint']);
+ $container->registerForAutoconfiguration(TestCase::class)
+ ->addTag('container.excluded', ['source' => 'because it\'s a test case']);
+ $container->registerForAutoconfiguration(\UnitEnum::class)
+ ->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']);
+ });
+ $container->registerAttributeForAutoconfiguration(\Attribute::class, static function (ChildDefinition $definition) {
+ $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']);
+ });
+ $container->registerAttributeForAutoconfiguration(Embeddable::class, static function (ChildDefinition $definition) {
+ $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']);
+ });
+
+ $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']);
+ });
+
if (!$container->getParameter('kernel.debug')) {
// remove tagged iterator argument for resource checkers
$container->getDefinition('config_cache_factory')->setArguments([]);
@@ -861,6 +969,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');
}
@@ -895,6 +1008,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');
}
@@ -1011,7 +1128,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');
@@ -1022,7 +1140,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;
@@ -1036,14 +1155,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) {
@@ -1052,21 +1183,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) {
@@ -1379,6 +1499,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');
+ }
}
/**
@@ -1557,6 +1697,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']);
@@ -1664,6 +1808,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');
@@ -1766,8 +1914,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))
;
}
@@ -1877,6 +2023,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');
@@ -1955,7 +2106,30 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
$container->setParameter('.serializer.named_serializers', $config['named_serializers'] ?? []);
}
- private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void
+ private function registerJsonStreamerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
+ {
+ 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_streamer.value_transformer');
+
+ $loader->load('json_streamer.php');
+
+ $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_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_streamer.cache_warmer.lazy_ghost');
+ }
+ }
+
+ 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".');
@@ -1963,18 +2137,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')) {
@@ -2112,9 +2292,14 @@ private function registerSchedulerConfiguration(ContainerBuilder $container, Php
if (!$this->hasConsole()) {
$container->removeDefinition('console.command.scheduler_debug');
}
+
+ // BC layer Scheduler < 7.3
+ if (!ContainerBuilder::willBeAvailable('symfony/serializer', DenormalizerInterface::class, ['symfony/framework-bundle', 'symfony/scheduler']) || !class_exists(SchedulerTriggerNormalizer::class)) {
+ $container->removeDefinition('serializer.normalizer.scheduler_trigger');
+ }
}
- 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".');
@@ -2130,6 +2315,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');
}
@@ -2169,6 +2358,13 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
['id' => 'handle_message'],
],
];
+
+ if ($lockEnabled && 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'];
@@ -2384,7 +2580,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));
}
@@ -2396,12 +2592,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';
@@ -2452,6 +2649,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']);
@@ -2667,6 +2868,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',
@@ -2697,6 +2899,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',
@@ -2743,6 +2946,46 @@ 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']['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']);
+ } 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.');
+ }
+ $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.listener');
+ }
+
if ($webhookEnabled) {
$loader->load('mailer_webhook.php');
}
@@ -2844,6 +3087,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',
@@ -2954,6 +3198,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',
@@ -3006,13 +3251,30 @@ 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;
+ }
+
+ unset($limiterConfig['limiters']);
+
+ $limiters[] = $name;
+
// default configuration (when used by other DI extensions)
$limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter'];
$limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter'))
->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));
@@ -3036,7 +3298,41 @@ 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/framework-bundle', '7.3', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName()));
+ $internalAliasId = \sprintf('.%s $%s.limiter', RateLimiterFactory::class, $name);
+
+ if ($container->hasAlias($internalAliasId)) {
+ $container->getAlias($internalAliasId)->setDeprecated('symfony/framework-bundle', '7.3', \sprintf('The "%%alias_id%%" autowiring alias is deprecated and will be removed in 8.0, use "%s $%s" instead.', RateLimiterFactoryInterface::class, (new Target($name.'.limiter'))->getParsedName()));
+ }
+ }
+ }
+
+ 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');
}
}
diff --git a/FrameworkBundle.php b/FrameworkBundle.php
index e83c4dfe6..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;
@@ -54,8 +55,10 @@
use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass;
use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass;
use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\JsonStreamer\DependencyInjection\StreamablePass;
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;
@@ -75,6 +78,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);
@@ -164,12 +168,14 @@ 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);
$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);
@@ -186,6 +192,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, StreamablePass::class);
if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2);
@@ -196,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/asset_mapper.php b/Resources/config/asset_mapper.php
index 404e7af18..eeb1ceb4f 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;
@@ -226,6 +232,7 @@
->args([
service('asset_mapper.importmap.manager'),
service('asset_mapper.importmap.version_checker'),
+ param('kernel.project_dir'),
])
->tag('console.command')
@@ -254,5 +261,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/cache.php b/Resources/config/cache.php
index ad4dca42d..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) {
@@ -140,6 +141,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 +158,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()
@@ -248,6 +251,8 @@
->alias(CacheInterface::class, 'cache.app')
+ ->alias(NamespacedPoolInterface::class, 'cache.app')
+
->alias(TagAwareCacheInterface::class, 'cache.app.taggable')
;
};
diff --git a/Resources/config/console.php b/Resources/config/console.php
index 9df82e20e..7ef10bb52 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;
@@ -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()
@@ -266,7 +268,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'),
@@ -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/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/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/mailer.php b/Resources/config/mailer.php
index f1dc560ab..43e7fb9a5 100644
--- a/Resources/config/mailer.php
+++ b/Resources/config/mailer.php
@@ -12,16 +12,21 @@
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\SMimeSigner;
return static function (ContainerConfigurator $container) {
$container->services()
@@ -74,6 +79,43 @@
->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('certificate'),
+ abstract_arg('key'),
+ abstract_arg('passphrase'),
+ abstract_arg('extraCertificates'),
+ abstract_arg('signOptions'),
+ ])
+
+ ->set('mailer.dkim_signer.listener', DkimSignedMessageListener::class)
+ ->args([
+ service('mailer.dkim_signer'),
+ ])
+ ->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.repository'),
+ param('mailer.smime_encrypter.cipher'),
+ ])
+ ->tag('kernel.event_subscriber')
+
->set('console.command.mailer_test', MailerTestCommand::class)
->args([
service('mailer.transports'),
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')])
diff --git a/Resources/config/messenger.php b/Resources/config/messenger.php
index 40f5b84ca..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;
@@ -25,6 +26,7 @@
use Symfony\Component\Messenger\EventListener\StopWorkerOnRestartSignalListener;
use Symfony\Component\Messenger\Handler\RedispatchMessageHandler;
use Symfony\Component\Messenger\Middleware\AddBusNameStampMiddleware;
+use Symfony\Component\Messenger\Middleware\DeduplicateMiddleware;
use Symfony\Component\Messenger\Middleware\DispatchAfterCurrentBusMiddleware;
use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware;
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
@@ -86,6 +88,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()
@@ -212,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'),
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,
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')
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/Resources/config/profiling.php b/Resources/config/profiling.php
index 4ae34649b..a81c53a63 100644
--- a/Resources/config/profiling.php
+++ b/Resources/config/profiling.php
@@ -12,10 +12,12 @@
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;
use Symfony\Component\HttpKernel\Profiler\Profiler;
+use Symfony\Component\HttpKernel\Profiler\ProfilerStateChecker;
return static function (ContainerConfigurator $container) {
$container->services()
@@ -56,5 +58,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()]),
+ inline_service('bool')->factory([FrameworkBundle::class, 'considerProfilerEnabled']),
+ ])
+
+ ->set('profiler.is_disabled_state_checker', 'Closure')
+ ->factory(['Closure', 'fromCallable'])
+ ->args([[service('profiler.state_checker'), 'isProfilerDisabled']])
;
};
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')
diff --git a/Resources/config/property_info.php b/Resources/config/property_info.php
index 90587839d..505dda6f4 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,10 +44,15 @@
->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])
->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])
;
};
diff --git a/Resources/config/routing/errors.php b/Resources/config/routing/errors.php
new file mode 100644
index 000000000..36a46dee4
--- /dev/null
+++ b/Resources/config/routing/errors.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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;
+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'])
+ ->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..177606b26
--- /dev/null
+++ b/Resources/config/routing/webhook.php
@@ -0,0 +1,30 @@
+
+ *
+ * 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;
+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' => '.+'])
+ ;
+};
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
- .+
-
+
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])
;
};
diff --git a/Resources/config/schema/symfony-1.0.xsd b/Resources/config/schema/symfony-1.0.xsd
index e99022acf..7f4b48a18 100644
--- a/Resources/config/schema/symfony-1.0.xsd
+++ b/Resources/config/schema/symfony-1.0.xsd
@@ -46,6 +46,7 @@
+
@@ -206,6 +207,7 @@
+
@@ -230,6 +232,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -244,6 +256,7 @@
+
@@ -273,6 +286,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -286,6 +317,7 @@
+
@@ -364,6 +396,7 @@
+
@@ -416,6 +449,7 @@
+
@@ -595,6 +629,7 @@
+
@@ -776,6 +811,9 @@
+
+
+
@@ -798,6 +836,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -998,4 +1060,9 @@
+
+
+
+
+
diff --git a/Resources/config/serializer.php b/Resources/config/serializer.php
index b291f51ac..e0a256bbe 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;
@@ -55,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()
@@ -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])
;
};
diff --git a/Resources/config/services.php b/Resources/config/services.php
index e5a86d8f4..936867d54 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;
@@ -157,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')
@@ -177,6 +181,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
->set('services_resetter', ServicesResetter::class)
->public()
+ ->alias(ServicesResetterInterface::class, 'services_resetter')
->set('reverse_container', ReverseContainer::class)
->args([
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/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',
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/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);
+ }
}
}
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);
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')
diff --git a/Tests/Console/Descriptor/AbstractDescriptorTestCase.php b/Tests/Console/Descriptor/AbstractDescriptorTestCase.php
index dde1f000b..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)
{
@@ -110,7 +125,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
@@ -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()) {
@@ -307,7 +323,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/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/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;
+ }
}
/**
diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php
index 6f3363f39..c8142e98a 100644
--- a/Tests/DependencyInjection/ConfigurationTest.php
+++ b/Tests/DependencyInjection/ConfigurationTest.php
@@ -15,12 +15,14 @@
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;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HttpClient\HttpClient;
+use Symfony\Component\JsonStreamer\JsonStreamWriter;
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -141,6 +143,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']);
@@ -778,12 +785,14 @@ protected static function getBundleDefaultConfig()
'localizable_html_attributes' => [],
],
'providers' => [],
+ 'globals' => [],
],
'validation' => [
'enabled' => !class_exists(FullStack::class),
'enable_attributes' => !class_exists(FullStack::class),
'static_method' => ['loadValidatorMetadata'],
'translation_domain' => 'validators',
+ 'disable_translation' => false,
'mapping' => [
'paths' => [],
],
@@ -817,7 +826,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,
@@ -863,6 +872,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' => [],
@@ -870,6 +884,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,
@@ -926,6 +941,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,
+ 'repository' => '',
+ 'cipher' => null,
+ ],
],
'notifier' => [
'enabled' => !class_exists(FullStack::class) && class_exists(Notifier::class),
@@ -975,6 +1011,9 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'remote-event' => [
'enabled' => !class_exists(FullStack::class) && class_exists(RemoteEvent::class),
],
+ 'json_streamer' => [
+ 'enabled' => !class_exists(FullStack::class) && class_exists(JsonStreamWriter::class),
+ ],
];
}
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 @@
+ true,
+ 'property_info' => [
+ 'enabled' => true,
+ 'with_constructor_extractor' => true,
+ ],
'type_info' => true,
'ide' => 'file%%link%%format',
'request' => [
diff --git a/Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php b/Tests/DependencyInjection/Fixtures/php/json_streamer.php
similarity index 75%
rename from Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php
rename to Tests/DependencyInjection/Fixtures/php/json_streamer.php
index 99e2a52cf..844b72004 100644
--- a/Tests/DependencyInjection/Fixtures/php/profiler_collect_serializer_data.php
+++ b/Tests/DependencyInjection/Fixtures/php/json_streamer.php
@@ -5,11 +5,10 @@
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
- 'profiler' => [
+ 'type_info' => [
'enabled' => true,
- 'collect_serializer_data' => true,
],
- 'serializer' => [
+ 'json_streamer' => [
'enabled' => true,
],
]);
diff --git a/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php b/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php
index 5c9c3c310..452594d45 100644
--- a/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.php
+++ b/Tests/DependencyInjection/Fixtures/php/messenger_bus_name_stamp.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/php/messenger_multiple_buses.php b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php
similarity index 97%
rename from Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php
rename to Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php
index b8e7530bb..f9b3767c0 100644
--- a/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses.php
+++ b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_with_deduplicate_middleware.php
@@ -5,6 +5,7 @@
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
+ 'lock' => null,
'messenger' => [
'default_bus' => 'messenger.bus.commands',
'buses' => [
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
new file mode 100644
index 000000000..fd4a00834
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/php/messenger_multiple_buses_without_deduplicate_middleware.php
@@ -0,0 +1,27 @@
+loadFromExtension('framework', [
+ 'annotations' => false,
+ 'http_method_override' => false,
+ 'handle_all_throwables' => true,
+ 'php_errors' => ['log' => true],
+ 'lock' => false,
+ '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/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/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/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/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/php/workflows.php b/Tests/DependencyInjection/Fixtures/php/workflows.php
index 118a627c7..2c29b8489 100644
--- a/Tests/DependencyInjection/Fixtures/php/workflows.php
+++ b/Tests/DependencyInjection/Fixtures/php/workflows.php
@@ -13,6 +13,9 @@
'supports' => [
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/full.xml b/Tests/DependencyInjection/Fixtures/xml/full.xml
index c01e85783..07faf22ab 100644
--- a/Tests/DependencyInjection/Fixtures/xml/full.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/full.xml
@@ -44,7 +44,8 @@
-
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/json_streamer.xml b/Tests/DependencyInjection/Fixtures/xml/json_streamer.xml
new file mode 100644
index 000000000..5c79cb840
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/xml/json_streamer.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml b/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
index bef24ed53..5e0b17851 100644
--- a/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/messenger_bus_name_stamp.xml
@@ -8,6 +8,7 @@
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml
similarity index 98%
rename from Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml
rename to Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml
index dcf402e1a..67decad20 100644
--- a/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_with_deduplicate_middleware.xml
@@ -8,6 +8,7 @@
+
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
new file mode 100644
index 000000000..3f0d96249
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_buses_without_deduplicate_middleware.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+ foo
+ true
+
+ baz
+
+
+
+
+
+
+
+
+
+
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/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/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/profiler_collect_serializer_data.xml b/Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml
similarity index 73%
rename from Tests/DependencyInjection/Fixtures/xml/profiler_collect_serializer_data.xml
rename to Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml
index 34d44d91c..6c686bd30 100644
--- a/Tests/DependencyInjection/Fixtures/xml/profiler_collect_serializer_data.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/translator_without_globals.xml
@@ -6,10 +6,9 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
-
+
-
-
+
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/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/full.yml b/Tests/DependencyInjection/Fixtures/yml/full.yml
index 7550749eb..8a1a3834b 100644
--- a/Tests/DependencyInjection/Fixtures/yml/full.yml
+++ b/Tests/DependencyInjection/Fixtures/yml/full.yml
@@ -64,9 +64,11 @@ framework:
default_context:
enable_max_depth: false
type_info: ~
- property_info: ~
+ property_info:
+ with_constructor_extractor: true
ide: file%%link%%format
request:
formats:
csv: ['text/csv', 'text/plain']
pdf: 'application/pdf'
+ json_streamer: ~
diff --git a/Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml b/Tests/DependencyInjection/Fixtures/yml/json_streamer.yml
similarity index 72%
rename from Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml
rename to Tests/DependencyInjection/Fixtures/yml/json_streamer.yml
index 5fe74b290..8873fea97 100644
--- a/Tests/DependencyInjection/Fixtures/yml/profiler_collect_serializer_data.yml
+++ b/Tests/DependencyInjection/Fixtures/yml/json_streamer.yml
@@ -4,8 +4,7 @@ framework:
handle_all_throwables: true
php_errors:
log: true
- serializer:
+ type_info:
enabled: true
- profiler:
+ json_streamer:
enabled: true
- collect_serializer_data: true
diff --git a/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml b/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
index 954c66ae9..79f8d7c87 100644
--- a/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
+++ b/Tests/DependencyInjection/Fixtures/yml/messenger_bus_name_stamp.yml
@@ -4,6 +4,7 @@ framework:
handle_all_throwables: true
php_errors:
log: true
+ lock: false
messenger:
default_bus: messenger.bus.commands
buses:
diff --git a/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml
similarity index 97%
rename from Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml
rename to Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml
index f06d534a5..ed52564c7 100644
--- a/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses.yml
+++ b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_with_deduplicate_middleware.yml
@@ -4,6 +4,7 @@ framework:
handle_all_throwables: true
php_errors:
log: true
+ lock: ~
messenger:
default_bus: messenger.bus.commands
buses:
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
new file mode 100644
index 000000000..38fca5737
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_buses_without_deduplicate_middleware.yml
@@ -0,0 +1,19 @@
+framework:
+ annotations: false
+ http_method_override: false
+ handle_all_throwables: true
+ php_errors:
+ log: true
+ lock: false
+ 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/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/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/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/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/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 c933f0edf..b5f5f1ef5 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;
@@ -63,6 +64,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;
@@ -84,11 +86,13 @@
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;
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;
@@ -276,25 +280,20 @@ 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');
+ DefinitionValidator::$called = false;
+
+ $container = $this->createContainerFromFile('workflows', compile: false);
+ $container->addCompilerPass(new WorkflowValidatorPass());
+ $container->compile();
$this->assertTrue($container->hasDefinition('workflow.article'), 'Workflow is registered as a service');
$this->assertSame('workflow.abstract', $container->getDefinition('workflow.article')->getParent());
@@ -317,6 +316,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');
@@ -410,7 +410,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 WorkflowValidatorPass());
+ $container->compile();
}
public function testWorkflowCannotHaveBothSupportsAndSupportStrategy()
@@ -615,21 +617,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]);
@@ -797,7 +803,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'));
@@ -1057,9 +1063,9 @@ 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));
@@ -1115,6 +1121,48 @@ public function testMessengerWithAddBusNameStampMiddleware()
], $container->getParameter('messenger.bus.events.middleware'));
}
+ 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([
+ ['id' => 'send_message', 'arguments' => []],
+ ['id' => 'handle_message', 'arguments' => []],
+ ], $container->getParameter('messenger.bus.queries.middleware'));
+
+ $this->assertTrue($container->hasAlias('messenger.default_bus'));
+ $this->assertSame('messenger.bus.commands', (string) $container->getAlias('messenger.default_bus'));
+ }
+
public function testMessengerMiddlewareFactoryErroneousFormat()
{
$this->expectException(\InvalidArgumentException::class);
@@ -1222,6 +1270,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');
@@ -1717,6 +1795,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()
@@ -1931,7 +2017,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()
@@ -2541,6 +2627,20 @@ public function testSemaphoreWithService()
self::assertEquals(new Reference('my_service'), $storeDef->getArgument(0));
}
+ public function testJsonStreamerEnabled()
+ {
+ $container = $this->createContainerFromFile('json_streamer');
+ $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([
diff --git a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
index ba600fe8d..c4f67c2f1 100644
--- a/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+++ b/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
@@ -17,8 +17,13 @@
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\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
{
@@ -100,7 +105,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,
@@ -126,9 +131,57 @@ public function testWorkflowValidationStateMachine()
],
],
]);
+ $container->addCompilerPass(new 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) {
@@ -189,7 +242,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) {
@@ -200,7 +253,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'],
],
]);
});
@@ -209,7 +262,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,
@@ -227,13 +283,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],
@@ -267,6 +345,92 @@ public function testRateLimiterIsTagged()
$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']],
+ ],
+ ]);
+ });
+
+ $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(
+ [
+ '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']],
+ ],
+ ]);
+ });
+ }
+
/**
* @dataProvider emailValidationModeProvider
*/
@@ -295,3 +459,14 @@ public static function emailValidationModeProvider()
yield ['loose'];
}
}
+
+class WorkflowValidatorWithConstructor implements DefinitionValidatorInterface
+{
+ public function __construct(bool $enabled)
+ {
+ }
+
+ public function validate(Definition $definition, string $name): void
+ {
+ }
+}
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 @@
[33mInformation for Service "[39m[32mservice_1[39m[33m"[39m
[33m===================================[39m
- ---------------- -----------------------------
- [32m Option [39m [32m Value [39m
- ---------------- -----------------------------
- 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
- ---------------- -----------------------------
+ ---------------- ---------------------------------
+ [32m Option [39m [32m Value [39m
+ ---------------- ---------------------------------
+ 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 @@
- ---------------- -----------------------------
- [32m Option [39m [32m Value [39m
- ---------------- -----------------------------
- 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
- ---------------- -----------------------------
-
+ ---------------- ---------------------------------
+ [32m Option [39m [32m Value [39m
+ ---------------- ---------------------------------
+ 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/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 @@
+ --------- ---------- ------------ ----------- ---------------
+ [32m Name [39m [32m Method [39m [32m Scheme [39m [32m Host [39m [32m Path [39m
+ --------- ---------- ------------ ----------- ---------------
+ 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 @@
+ --------- ---------- ------------ ----------- -----------
+ [32m Name [39m [32m Method [39m [32m Scheme [39m [32m Host [39m [32m Path [39m
+ --------- ---------- ------------ ----------- -----------
+ 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']
+
+
diff --git a/Tests/Fixtures/Messenger/DummyCommand.php b/Tests/Fixtures/Messenger/DummyCommand.php
new file mode 100644
index 000000000..c8f800850
--- /dev/null
+++ b/Tests/Fixtures/Messenger/DummyCommand.php
@@ -0,0 +1,30 @@
+addArgument('dummy-argument', InputArgument::OPTIONAL);
+ }
+
+ public function execute(InputInterface $input, ?OutputInterface $output = null): int
+ {
+ self::$calls[__FUNCTION__][] = $input->getArgument('dummy-argument');
+
+ return Command::SUCCESS;
+ }
+}
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..3321e28d1
--- /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 $source, ?object $target): mixed
+ {
+ return 'transformed';
+ }
+}
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());
}
diff --git a/Tests/Functional/ContainerDebugCommandTest.php b/Tests/Functional/ContainerDebugCommandTest.php
index 95dcc36ed..d21d4d113 100644
--- a/Tests/Functional/ContainerDebugCommandTest.php
+++ b/Tests/Functional/ContainerDebugCommandTest.php
@@ -345,4 +345,22 @@ public static function provideCompletionSuggestions(): iterable
['txt', 'xml', 'json', 'md'],
];
}
+
+ 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'));
+ @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());
+ }
}
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/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/SchedulerTest.php b/Tests/Functional/SchedulerTest.php
index 99776e822..537493a55 100644
--- a/Tests/Functional/SchedulerTest.php
+++ b/Tests/Functional/SchedulerTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage;
+use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyCommand;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummySchedule;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyTask;
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage;
@@ -88,6 +89,29 @@ public function testAutoconfiguredScheduler()
$this->assertSame([['5', 6], ['7', 8]], $calls['attributesOnMethod']);
}
+ public function testAutoconfiguredSchedulerCommand()
+ {
+ $container = self::getContainer();
+ $container->set('clock', $clock = new MockClock('2023-10-26T08:59:59Z'));
+
+ $this->assertTrue($container->get('receivers')->has('scheduler_dummy_command'));
+ $this->assertInstanceOf(SchedulerTransport::class, $cron = $container->get('receivers')->get('scheduler_dummy_command'));
+ $bus = $container->get(MessageBusInterface::class);
+
+ $getCalls = static function (float $sleep) use ($clock, $cron, $bus) {
+ DummyCommand::$calls = [];
+ $clock->sleep($sleep);
+ foreach ($cron->get() as $message) {
+ $bus->dispatch($message->with(new ReceivedStamp('scheduler_dummy_command')));
+ }
+
+ return DummyCommand::$calls;
+ };
+
+ $this->assertSame([], $getCalls(0));
+ $this->assertSame(['execute' => [0 => null, 1 => 'test']], $getCalls(1));
+ }
+
public function testSchedulerWithCustomTransport()
{
$container = self::getContainer();
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/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/JsonStreamer/RangeToStringValueTransformer.php b/Tests/Functional/app/JsonStreamer/RangeToStringValueTransformer.php
new file mode 100644
index 000000000..6d21f2d2f
--- /dev/null
+++ b/Tests/Functional/app/JsonStreamer/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\JsonStreamer;
+
+use Symfony\Component\JsonStreamer\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 getStreamValueType(): BuiltinType
+ {
+ return Type::string();
+ }
+}
diff --git a/Tests/Functional/app/JsonStreamer/StringToRangeValueTransformer.php b/Tests/Functional/app/JsonStreamer/StringToRangeValueTransformer.php
new file mode 100644
index 000000000..398beb2ff
--- /dev/null
+++ b/Tests/Functional/app/JsonStreamer/StringToRangeValueTransformer.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\JsonStreamer;
+
+use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
+use Symfony\Component\TypeInfo\Type;
+use Symfony\Component\TypeInfo\Type\BuiltinType;
+
+/**
+ * @author Mathias Arlaud
+ */
+class StringToRangeValueTransformer implements ValueTransformerInterface
+{
+ public function transform(mixed $value, array $options = []): array
+ {
+ return array_map(static fn (string $v): int => (int) $v, explode('..', $value));
+ }
+
+ public static function getStreamValueType(): BuiltinType
+ {
+ return Type::string();
+ }
+}
diff --git a/Tests/Functional/app/JsonStreamer/bundles.php b/Tests/Functional/app/JsonStreamer/bundles.php
new file mode 100644
index 000000000..15ff182c6
--- /dev/null
+++ b/Tests/Functional/app/JsonStreamer/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/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/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/Tests/Functional/app/Scheduler/config.yml b/Tests/Functional/app/Scheduler/config.yml
index bd1cb6516..f5bc14ec4 100644
--- a/Tests/Functional/app/Scheduler/config.yml
+++ b/Tests/Functional/app/Scheduler/config.yml
@@ -16,6 +16,9 @@ services:
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyTaskWithCustomReceiver:
autoconfigure: true
+ Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyCommand:
+ autoconfigure: true
+
clock:
synthetic: 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:
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 }
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..159dd21eb 100644
--- a/Tests/Kernel/MicroKernelTraitTest.php
+++ b/Tests/Kernel/MicroKernelTraitTest.php
@@ -13,7 +13,10 @@
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
-use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+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 +155,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);
@@ -165,27 +185,3 @@ public function testDefaultKernel()
$this->assertSame('OK', $response->getContent());
}
}
-
-abstract class MinimalKernel extends Kernel
-{
- use MicroKernelTrait;
-
- private string $cacheDir;
-
- public function __construct(string $cacheDir)
- {
- parent::__construct('test', false);
-
- $this->cacheDir = sys_get_temp_dir().'/'.$cacheDir;
- }
-
- public function getCacheDir(): string
- {
- return $this->cacheDir;
- }
-
- public function getLogDir(): string
- {
- return $this->cacheDir;
- }
-}
diff --git a/Tests/Kernel/MinimalKernel.php b/Tests/Kernel/MinimalKernel.php
new file mode 100644
index 000000000..df2c97e6a
--- /dev/null
+++ b/Tests/Kernel/MinimalKernel.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel;
+
+use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
+use Symfony\Component\HttpKernel\Kernel;
+
+abstract class MinimalKernel extends Kernel
+{
+ use MicroKernelTrait;
+
+ private string $cacheDir;
+
+ public function __construct(string $cacheDir)
+ {
+ parent::__construct('test', false);
+
+ $this->cacheDir = sys_get_temp_dir().'/'.$cacheDir;
+ }
+
+ public function getCacheDir(): string
+ {
+ return $this->cacheDir;
+ }
+
+ public function getLogDir(): string
+ {
+ return $this->cacheDir;
+ }
+}
diff --git a/composer.json b/composer.json
index 3b2d2806d..a00bac1c3 100644
--- a/composer.json
+++ b/composer.json
@@ -20,12 +20,12 @@
"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",
+ "symfony/error-handler": "^7.3",
"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",
@@ -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": "^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",
@@ -62,13 +63,14 @@
"symfony/serializer": "^7.2.5",
"symfony/stopwatch": "^6.4|^7.0",
"symfony/string": "^6.4|^7.0",
- "symfony/translation": "^6.4|^7.0",
+ "symfony/translation": "^7.3",
"symfony/twig-bundle": "^6.4|^7.0",
"symfony/type-info": "^7.1.8",
"symfony/validator": "^6.4|^7.0",
- "symfony/workflow": "^6.4|^7.0",
+ "symfony/workflow": "^7.3",
"symfony/yaml": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
+ "symfony/json-streamer": "7.3.*",
"symfony/uid": "^6.4|^7.0",
"symfony/web-link": "^6.4|^7.0",
"symfony/webhook": "^7.2",
@@ -87,10 +89,12 @@
"symfony/dom-crawler": "<6.4",
"symfony/http-client": "<6.4",
"symfony/form": "<6.4",
+ "symfony/json-streamer": ">=7.4",
"symfony/lock": "<6.4",
"symfony/mailer": "<6.4",
"symfony/messenger": "<6.4",
"symfony/mime": "<6.4",
+ "symfony/object-mapper": ">=7.4",
"symfony/property-info": "<6.4",
"symfony/property-access": "<6.4",
"symfony/runtime": "<6.4.13|>=7.0,<7.1.6",
@@ -99,13 +103,13 @@
"symfony/security-core": "<6.4",
"symfony/serializer": "<7.2.5",
"symfony/stopwatch": "<6.4",
- "symfony/translation": "<6.4",
+ "symfony/translation": "<7.3",
"symfony/twig-bridge": "<6.4",
"symfony/twig-bundle": "<6.4",
"symfony/validator": "<6.4",
"symfony/web-profiler-bundle": "<6.4",
"symfony/webhook": "<7.2",
- "symfony/workflow": "<6.4"
+ "symfony/workflow": "<7.3.0-beta2"
},
"autoload": {
"psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" },