diff --git a/Command/BuildDebugContainerTrait.php b/Command/BuildDebugContainerTrait.php index f1e32f829..364fcb1f6 100644 --- a/Command/BuildDebugContainerTrait.php +++ b/Command/BuildDebugContainerTrait.php @@ -60,7 +60,10 @@ protected function getContainerBuilder(KernelInterface $kernel): ContainerBuilde $dumpedContainer = unserialize(file_get_contents(substr_replace($file, '.ser', -4))); $container->setDefinitions($dumpedContainer->getDefinitions()); $container->setAliases($dumpedContainer->getAliases()); - $container->__construct($dumpedContainer->getParameterBag()); + + $parameterBag = $container->getParameterBag(); + $parameterBag->clear(); + $parameterBag->add($dumpedContainer->getParameterBag()->all()); } return $this->container = $container; diff --git a/Command/ConfigDebugCommand.php b/Command/ConfigDebugCommand.php index 50c8dddf5..93b3b9068 100644 --- a/Command/ConfigDebugCommand.php +++ b/Command/ConfigDebugCommand.php @@ -161,7 +161,7 @@ private function getConfigForPath(array $config, string $path, string $alias): m $steps = explode('.', $path); foreach ($steps as $step) { - if (!\array_key_exists($step, $config)) { + if (!\is_array($config) || !\array_key_exists($step, $config)) { throw new LogicException(\sprintf('Unable to find configuration for "%s.%s".', $alias, $path)); } diff --git a/Controller/AbstractController.php b/Controller/AbstractController.php index 76358f41e..3c7478c6a 100644 --- a/Controller/AbstractController.php +++ b/Controller/AbstractController.php @@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Flow\FormFlowInterface; +use Symfony\Component\Form\Flow\FormFlowTypeInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; @@ -344,6 +346,8 @@ protected function createAccessDeniedException(string $message = 'Access Denied. /** * Creates and returns a Form instance from the type of the form. + * + * @return ($type is class-string ? FormFlowInterface : FormInterface) */ protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { diff --git a/Controller/ControllerHelper.php b/Controller/ControllerHelper.php index 14e20fa32..3ba0a47bc 100644 --- a/Controller/ControllerHelper.php +++ b/Controller/ControllerHelper.php @@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Flow\FormFlowInterface; +use Symfony\Component\Form\Flow\FormFlowTypeInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; @@ -337,6 +339,8 @@ public function createAccessDeniedException(string $message = 'Access Denied.', /** * Creates and returns a Form instance from the type of the form. + * + * @return ($type is class-string ? FormFlowInterface : FormInterface) */ public function createForm(string $type, mixed $data = null, array $options = []): FormInterface { diff --git a/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php b/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php index 5c468a562..9a0bf4093 100644 --- a/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php +++ b/DependencyInjection/Compiler/PhpConfigReferenceDumpPass.php @@ -95,15 +95,7 @@ public function process(ContainerBuilder $container): void $appTypes = ''; $anyEnvExtensions = []; - foreach ($container->getExtensions() as $alias => $extension) { - if (!$configuration = $this->getConfiguration($extension, $container)) { - continue; - } - - $anyEnvExtensions[$alias] = $extension; - $type = $this->camelCase($alias).'Config'; - $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree())); - } + $registeredExtensions = $container->getExtensions(); foreach ($this->bundlesDefinition as $bundle => $envs) { if (!is_subclass_of($bundle, BundleInterface::class)) { continue; @@ -111,19 +103,20 @@ public function process(ContainerBuilder $container): void if (!$extension = (new $bundle())->getContainerExtension()) { continue; } - if (!$configuration = $this->getConfiguration($extension, $container)) { - continue; - } $extensionAlias = $extension->getAlias(); - if (isset($anyEnvExtensions[$extensionAlias])) { - $extension = $anyEnvExtensions[$extensionAlias]; - } else { - $anyEnvExtensions[$extensionAlias] = $extension; - $type = $this->camelCase($extensionAlias).'Config'; - $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree())); + if (isset($registeredExtensions[$extensionAlias])) { + $extension = $registeredExtensions[$extensionAlias]; + unset($registeredExtensions[$extensionAlias]); } + if (!$configuration = $this->getConfiguration($extension, $container)) { + continue; + } + $anyEnvExtensions[$extensionAlias] = $extension; + $type = $this->camelCase($extensionAlias).'Config'; + $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree())); + foreach ($knownEnvs as $env) { if ($envs[$env] ?? $envs['all'] ?? false) { $extensionsPerEnv[$env][] = $extension; @@ -132,6 +125,14 @@ public function process(ContainerBuilder $container): void } } } + foreach ($registeredExtensions as $alias => $extension) { + if (!$configuration = $this->getConfiguration($extension, $container)) { + continue; + } + $anyEnvExtensions[$alias] = $extension; + $type = $this->camelCase($alias).'Config'; + $appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree())); + } krsort($extensionsPerEnv); $r = new \ReflectionClass(AppReference::class); diff --git a/DependencyInjection/Compiler/UnusedTagsPass.php b/DependencyInjection/Compiler/UnusedTagsPass.php index 36e3ee1ae..c07116e26 100644 --- a/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/DependencyInjection/Compiler/UnusedTagsPass.php @@ -70,6 +70,8 @@ class UnusedTagsPass implements CompilerPassInterface 'mime.mime_type_guesser', 'monolog.logger', 'notifier.channel', + 'object_mapper.condition_callable', + 'object_mapper.transform_callable', 'property_info.access_extractor', 'property_info.constructor_extractor', 'property_info.initializable_extractor', @@ -108,8 +110,6 @@ 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 02f5f5bf7..179d28850 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -462,8 +462,19 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void ->cannotBeEmpty() ->end() ->arrayNode('initial_marking') - ->acceptAndWrap(['string']) + ->acceptAndWrap(['backed-enum', 'string']) ->defaultValue([]) + ->beforeNormalization() + ->ifArray() + ->then(static function ($markings) { + $normalizedMarkings = []; + foreach ($markings as $marking) { + $normalizedMarkings[] = $marking instanceof \BackedEnum ? $marking->value : $marking; + } + + return $normalizedMarkings; + }) + ->end() ->prototype('scalar')->end() ->end() ->arrayNode('events_to_dispatch', 'event_to_dispatch') @@ -1936,6 +1947,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->arrayNode('vars', 'var') ->info('Associative array: the default vars used to expand the templated URI.') + ->useAttributeAsKey('name') ->normalizeKeys(false) ->variablePrototype()->end() ->end() @@ -2014,6 +2026,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->arrayNode('extra') ->info('Extra options for specific HTTP client.') + ->useAttributeAsKey('name') ->normalizeKeys(false) ->variablePrototype()->end() ->end() @@ -2160,6 +2173,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->end() ->arrayNode('extra') ->info('Extra options for specific HTTP client.') + ->useAttributeAsKey('name') ->normalizeKeys(false) ->variablePrototype()->end() ->end() diff --git a/DependencyInjection/FrameworkExtension.php b/DependencyInjection/FrameworkExtension.php index cb0427c78..01d64ee61 100644 --- a/DependencyInjection/FrameworkExtension.php +++ b/DependencyInjection/FrameworkExtension.php @@ -1692,11 +1692,11 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder foreach ($config['providers'] as $provider) { if ($provider['locales']) { - $locales += $provider['locales']; + $locales = array_merge($locales, $provider['locales']); } } - $locales = array_unique($locales); + $locales = array_values(array_unique($locales)); $container->getDefinition('console.command.translation_pull') ->replaceArgument(4, array_merge($transPaths, [$config['default_path']])) @@ -1752,8 +1752,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder if (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug')) { // The $reflector argument hints at where the attribute could be used $container->registerAttributeForAutoconfiguration(Constraint::class, static function (ChildDefinition $definition, Constraint $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) { - $definition->addTag('validator.attribute_metadata') - ->addTag('container.excluded', ['source' => 'because it\'s a validator constraint extension']); + $definition->addTag('validator.attribute_metadata'); }); } @@ -2005,15 +2004,13 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder if (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug')) { // The $reflector argument hints at where the attribute could be used $configurator = static function (ChildDefinition $definition, object $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) { - $definition->addTag('serializer.attribute_metadata') - ->addTag('container.excluded', ['source' => 'because it\'s a serializer metadata extension']); + $definition->addTag('serializer.attribute_metadata'); }; $container->registerAttributeForAutoconfiguration(SerializerMapping\Context::class, $configurator); $container->registerAttributeForAutoconfiguration(SerializerMapping\Groups::class, $configurator); $configurator = static function (ChildDefinition $definition, object $attribute, \ReflectionMethod|\ReflectionProperty $reflector) { - $definition->addTag('serializer.attribute_metadata') - ->addTag('container.excluded', ['source' => 'because it\'s a serializer metadata extension']); + $definition->addTag('serializer.attribute_metadata'); }; $container->registerAttributeForAutoconfiguration(SerializerMapping\Ignore::class, $configurator); $container->registerAttributeForAutoconfiguration(SerializerMapping\MaxDepth::class, $configurator); @@ -2021,8 +2018,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedPath::class, $configurator); $container->registerAttributeForAutoconfiguration(SerializerMapping\DiscriminatorMap::class, static function (ChildDefinition $definition) { - $definition->addTag('serializer.attribute_metadata') - ->addTag('container.excluded', ['source' => 'because it\'s a serializer metadata extension']); + $definition->addTag('serializer.attribute_metadata'); }); } @@ -2729,12 +2725,12 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder unset($scopeConfig['retry_failed']); // This "transport" service is decorated in the following order: - // 1. ThrottlingHttpClient (30) -> throttles requests - // 2. UriTemplateHttpClient (25) -> expands URI templates - // 3. ScopingHttpClient (20) -> resolves relative URLs and applies scope configuration - // 4. CachingHttpClient (15) -> caches responses - // 5. RetryableHttpClient (10) -> retries requests - // 6. TraceableHttpClient (5) -> traces requests + // 1. ThrottlingHttpClient (5) -> throttles requests + // 2. UriTemplateHttpClient (10) -> expands URI templates + // 3. ScopingHttpClient (15) -> resolves relative URLs and applies scope configuration + // 4. CachingHttpClient (20) -> caches responses + // 5. RetryableHttpClient (25) -> retries requests + // 6. TraceableHttpClient (100) -> traces requests $container->register($name, HttpClientInterface::class) ->setFactory('current') ->setArguments([[new Reference('http_client.transport')]]) @@ -2742,7 +2738,7 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder ; $scopingDefinition = $container->register($name.'.scoping', ScopingHttpClient::class) - ->setDecoratedService($name, null, 20) + ->setDecoratedService($name, null, 15) ->addTag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore']); if (null === $scope) { @@ -2771,7 +2767,7 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container ->register($name.'.uri_template', UriTemplateHttpClient::class) - ->setDecoratedService($name, null, 25) + ->setDecoratedService($name, null, 10) ->setArguments([ new Reference('.inner'), new Reference('http_client.uri_template_expander', ContainerInterface::NULL_ON_INVALID_REFERENCE), @@ -2810,7 +2806,7 @@ private function registerCachingHttpClient(array $options, array $defaultOptions $container ->register($name.'.caching', CachingHttpClient::class) - ->setDecoratedService($name, null, 15) + ->setDecoratedService($name, null, 20) ->setArguments([ new Reference('.inner'), new Reference($options['cache_pool']), @@ -2831,7 +2827,7 @@ private function registerThrottlingHttpClient(string $rateLimiter, string $name, $container ->register($name.'.throttling', ThrottlingHttpClient::class) - ->setDecoratedService($name, null, 30) + ->setDecoratedService($name, null, 5) ->setArguments([new Reference('.inner'), new Reference($name.'.throttling.limiter')]); } @@ -2863,7 +2859,7 @@ private function registerRetryableHttpClient(array $options, string $name, Conta $container ->register($name.'.retryable', RetryableHttpClient::class) - ->setDecoratedService($name, null, 10) + ->setDecoratedService($name, null, 25) ->setArguments([new Reference('.inner'), $retryStrategy, $options['max_retries'], new Reference('logger')]) ->addTag('monolog.logger', ['channel' => 'http_client']); } diff --git a/FrameworkBundle.php b/FrameworkBundle.php index 3ad1dedc2..b892a825d 100644 --- a/FrameworkBundle.php +++ b/FrameworkBundle.php @@ -149,9 +149,6 @@ public function build(ContainerBuilder $container): void ]); } - if ($container->hasParameter('.kernel.config_dir') && $container->hasParameter('.kernel.bundles_definition')) { - $container->addCompilerPass(new PhpConfigReferenceDumpPass($container->getParameter('.kernel.config_dir').'/reference.php', $container->getParameter('.kernel.bundles_definition'))); - } $container->addCompilerPass(new AssetsContextPass()); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); @@ -208,6 +205,9 @@ public function build(ContainerBuilder $container): void $this->addCompilerPassIfExists($container, StreamablePass::class); if ($container->getParameter('kernel.debug')) { + if ($container->hasParameter('.kernel.config_dir') && $container->hasParameter('.kernel.bundles_definition')) { + $container->addCompilerPass(new PhpConfigReferenceDumpPass($container->getParameter('.kernel.config_dir').'/reference.php', $container->getParameter('.kernel.bundles_definition'))); + } $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_BEFORE_REMOVING, -255); diff --git a/Resources/config/console.php b/Resources/config/console.php index dd3f16a7a..2641586c2 100644 --- a/Resources/config/console.php +++ b/Resources/config/console.php @@ -201,7 +201,7 @@ service('messenger.routable_message_bus'), service('event_dispatcher'), service('logger')->nullOnInvalid(), - service('messenger.transport.native_php_serializer')->nullOnInvalid(), + service('.messenger.transport.native_php_serializer')->nullOnInvalid(), null, ]) ->tag('console.command') @@ -211,7 +211,7 @@ ->args([ abstract_arg('Default failure receiver name'), abstract_arg('Receivers'), - service('messenger.transport.native_php_serializer')->nullOnInvalid(), + service('.messenger.transport.native_php_serializer')->nullOnInvalid(), ]) ->tag('console.command') @@ -219,7 +219,7 @@ ->args([ abstract_arg('Default failure receiver name'), abstract_arg('Receivers'), - service('messenger.transport.native_php_serializer')->nullOnInvalid(), + service('.messenger.transport.native_php_serializer')->nullOnInvalid(), ]) ->tag('console.command') diff --git a/Resources/config/messenger.php b/Resources/config/messenger.php index be635a0f4..c4d4f8d19 100644 --- a/Resources/config/messenger.php +++ b/Resources/config/messenger.php @@ -78,7 +78,10 @@ ->set('serializer.normalizer.flatten_exception', FlattenExceptionNormalizer::class) ->tag('serializer.normalizer', ['built_in' => true, 'priority' => -880]) + ->set('.messenger.transport.native_php_serializer', PhpSerializer::class) ->set('messenger.transport.native_php_serializer', PhpSerializer::class) + ->factory('current') + ->args([[service('.messenger.transport.native_php_serializer')]]) ->alias('messenger.default_serializer', 'messenger.transport.native_php_serializer') ->alias(SerializerInterface::class, 'messenger.default_serializer') diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 4999adb11..b644041ae 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -632,6 +632,7 @@ public function testWorkflowEnumArcsNormalization() 'enum' => [ 'supports' => [self::class], 'places' => Places::cases(), + 'initial_marking' => Places::A, 'transitions' => [ [ 'name' => 'one', @@ -649,6 +650,8 @@ public function testWorkflowEnumArcsNormalization() ], ]]); + $this->assertSame(['a'], $config['workflows']['workflows']['enum']['initial_marking']); + $transitions = $config['workflows']['workflows']['enum']['transitions']; $this->assertSame('one', $transitions[0]['name']); diff --git a/Tests/DependencyInjection/Fixtures/php/translator_providers.php b/Tests/DependencyInjection/Fixtures/php/translator_providers.php new file mode 100644 index 000000000..9f8d36631 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/php/translator_providers.php @@ -0,0 +1,15 @@ +loadFromExtension('framework', [ + 'enabled_locales' => ['es'], + 'translator' => [ + 'providers' => [ + 'foo_provider' => [ + 'locales' => ['en', 'fr'], + ], + 'bar_provider' => [ + 'locales' => ['de', 'pl'], + ] + ] + ], +]); diff --git a/Tests/DependencyInjection/Fixtures/yml/translator_providers.yml b/Tests/DependencyInjection/Fixtures/yml/translator_providers.yml new file mode 100644 index 000000000..cb229904f --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/yml/translator_providers.yml @@ -0,0 +1,8 @@ +framework: + enabled_locales: [ 'es' ] + translator: + providers: + foo_provider: + locales: [ 'en', 'fr' ] + bar_provider: + locales: [ 'de', 'pl' ] diff --git a/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/Tests/DependencyInjection/FrameworkExtensionTestCase.php index d4c641eaa..5ed6f4dd2 100644 --- a/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1394,6 +1394,13 @@ public function testTranslator() $this->assertSame('Fixtures/translations', $options['cache_vary']['scanned_directories'][3]); } + public function testTranslatorProvidersMergedEnabledLocales() + { + $container = $this->createContainerFromFile('translator_providers'); + self::assertSame(['es', 'en', 'fr', 'de', 'pl'], $container->getDefinition('console.command.translation_pull')->getArgument(5)); + self::assertSame(['es', 'en', 'fr', 'de', 'pl'], $container->getDefinition('console.command.translation_push')->getArgument(3)); + } + public function testTranslatorMultipleFallbacks() { $container = $this->createContainerFromFile('translator_fallbacks'); diff --git a/Tests/Functional/ConfigDebugCommandTest.php b/Tests/Functional/ConfigDebugCommandTest.php index ea4ee0788..259a9e738 100644 --- a/Tests/Functional/ConfigDebugCommandTest.php +++ b/Tests/Functional/ConfigDebugCommandTest.php @@ -233,6 +233,16 @@ public static function provideCompletionSuggestions(): \Generator yield 'option --format, debug' => [true, ['--format', ''], ['yaml', 'json']]; } + public function testDumpPathDeepIntoScalar() + { + $tester = $this->createCommandTester(true); + + $tester->execute(['name' => 'framework', 'path' => 'secret.foo']); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertStringContainsString('Unable to find configuration for "framework.secret.foo"', $tester->getDisplay()); + } + private function createCommandTester(bool $debug): CommandTester { $command = $this->createApplication($debug)->find('debug:config'); diff --git a/Tests/Functional/PropertyInfoTest.php b/Tests/Functional/PropertyInfoTest.php index 041400a0c..745cae28d 100644 --- a/Tests/Functional/PropertyInfoTest.php +++ b/Tests/Functional/PropertyInfoTest.php @@ -21,7 +21,7 @@ public function testPhpDocPriority() $propertyInfo = static::getContainer()->get('property_info'); - $this->assertEquals(Type::list(Type::int()), $propertyInfo->getType(Dummy::class, 'codes')); + $this->assertEquals(Type::array(Type::int()), $propertyInfo->getType(Dummy::class, 'codes')); } } diff --git a/composer.json b/composer.json index b70fcd291..efe01386e 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "doctrine/persistence": "^1.3|^2|^3", "dragonmantank/cron-expression": "^3.1", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", "seld/jsonlint": "^1.10", "symfony/asset": "^7.4|^8.0", "symfony/asset-mapper": "^7.4|^8.0", @@ -70,7 +71,7 @@ "symfony/string": "^7.4|^8.0", "symfony/translation": "^7.4|^8.0", "symfony/twig-bundle": "^7.4|^8.0", - "symfony/type-info": "^7.4|^8.0", + "symfony/type-info": "^7.4.1|^8.0.1", "symfony/uid": "^7.4|^8.0", "symfony/validator": "^7.4|^8.0", "symfony/workflow": "^7.4|^8.0",