diff --git a/composer.json b/composer.json index 5e0de11510edf..31915b8aae76f 100644 --- a/composer.json +++ b/composer.json @@ -197,6 +197,10 @@ "Symfony\\Bridge\\Twig\\": "src/Symfony/Bridge/Twig/", "Symfony\\Bundle\\": "src/Symfony/Bundle/", "Symfony\\Component\\": "src/Symfony/Component/", + "Symfony\\Config\\": [ + "src/Symfony/Component/DependencyInjection/Loader/Config/", + "src/Symfony/Component/Routing/Loader/Config/" + ], "Symfony\\Runtime\\Symfony\\Component\\": "src/Symfony/Component/Runtime/Internal/" }, "files": [ diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index a7649e3318708..3b1ed7ad4aa36 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * Allow multiple `#[AsDecorator]` attributes * Handle returning arrays and config-builders from config files * Handle declaring services using PHP arrays that follow the same shape as corresponding yaml files + * Add `ServicesConfig` to help writing PHP configs using yaml-like array-shapes * Deprecate using `$this` or its internal scope from PHP config files; use the `$loader` variable instead * Deprecate XML configuration format, use YAML or PHP instead * Deprecate `ExtensionInterface::getXsdValidationBasePath()` and `getNamespace()` diff --git a/src/Symfony/Component/DependencyInjection/Loader/Config/ServicesConfig.php b/src/Symfony/Component/DependencyInjection/Loader/Config/ServicesConfig.php new file mode 100644 index 0000000000000..b73b207a40e26 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Loader/Config/ServicesConfig.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Config; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\Expression; + +require_once __DIR__.\DIRECTORY_SEPARATOR.'functions.php'; + +/** + * @psalm-type Arguments = list|array + * @psalm-type Callback = string|array{0:string|Reference|ReferenceConfigurator,1:string}|\Closure|Reference|ReferenceConfigurator|Expression + * @psalm-type Tags = list>> + * @psalm-type Deprecation = array{package: string, version: string, message?: string} + * @psalm-type Call = array|array{0:string, 1?:Arguments, 2?:bool}|array{method:string, arguments?:Arguments, returns_clone?:bool} + * @psalm-type Imports = list + * @psalm-type Parameters = array|null> + * @psalm-type Defaults = array{ + * public?: bool, + * tags?: Tags, + * resource_tags?: Tags, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type Instanceof = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: Callback, + * calls?: list, + * tags?: Tags, + * resource_tags?: Tags, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type Definition = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: Deprecation, + * factory?: Callback, + * configurator?: Callback, + * arguments?: Arguments, + * properties?: array, + * calls?: list, + * tags?: Tags, + * resource_tags?: Tags, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null|ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE|ContainerInterface::IGNORE_ON_INVALID_REFERENCE|ContainerInterface::NULL_ON_INVALID_REFERENCE, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: mixed, + * } + * @psalm-type Alias = string|array{ + * alias: string, + * public?: bool, + * deprecated?: Deprecation, + * } + * @psalm-type Prototype = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: Deprecation, + * factory?: Callback, + * arguments?: Arguments, + * properties?: array, + * configurator?: Callback, + * calls?: list, + * tags?: Tags, + * resource_tags?: Tags, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type Stack = array{ + * stack: list>, + * public?: bool, + * deprecated?: Deprecation, + * } + * @psalm-type Services = array|array + */ +class ServicesConfig +{ + public readonly array $services; + + /** + * @param Services $services + * @param Imports $imports + * @param Parameters $parameters + * @param Defaults $defaults + * @param Instanceof $instanceof + */ + public function __construct( + array $services = [], + public readonly array $imports = [], + public readonly array $parameters = [], + array $defaults = [], + array $instanceof = [], + ) { + if (isset($services['_defaults']) || isset($services['_instanceof'])) { + throw new InvalidArgumentException('The $services argument should not contain "_defaults" or "_instanceof" keys, use the $defaults and $instanceof parameters instead.'); + } + + $services['_defaults'] = $defaults; + $services['_instanceof'] = $instanceof; + $this->services = $services; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/Config/functions.php b/src/Symfony/Component/DependencyInjection/Loader/Config/functions.php new file mode 100644 index 0000000000000..734c66dca7170 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Loader/Config/functions.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Config; + +use Symfony\Component\Config\Loader\ParamConfigurator; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ClosureReferenceConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\EnvConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\InlineServiceConfigurator; +use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator; +use Symfony\Component\ExpressionLanguage\Expression; + +/** + * Creates a parameter. + */ +function param(string $name): ParamConfigurator +{ + return new ParamConfigurator($name); +} + +/** + * Creates a reference to a service. + */ +function service(string $serviceId): ReferenceConfigurator +{ + return new ReferenceConfigurator($serviceId); +} + +/** + * Creates an inline service. + */ +function inline_service(?string $class = null): InlineServiceConfigurator +{ + return new InlineServiceConfigurator(new Definition($class)); +} + +/** + * Creates a service locator. + * + * @param array $values + */ +function service_locator(array $values): ServiceLocatorArgument +{ + $values = AbstractConfigurator::processValue($values, true); + + return new ServiceLocatorArgument($values); +} + +/** + * Creates a lazy iterator. + * + * @param ReferenceConfigurator[] $values + */ +function iterator(array $values): IteratorArgument +{ + return new IteratorArgument(AbstractConfigurator::processValue($values, true)); +} + +/** + * Creates a lazy iterator by tag name. + */ +function tagged_iterator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): TaggedIteratorArgument +{ + return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf); +} + +/** + * Creates a service locator by tag name. + */ +function tagged_locator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): ServiceLocatorArgument +{ + return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)); +} + +/** + * Creates an expression. + */ +function expr(string $expression): Expression +{ + return new Expression($expression); +} + +/** + * Creates an abstract argument. + */ +function abstract_arg(string $description): AbstractArgument +{ + return new AbstractArgument($description); +} + +/** + * Creates an environment variable reference. + */ +function env(string $name): EnvConfigurator +{ + return new EnvConfigurator($name); +} + +/** + * Creates a closure service reference. + */ +function service_closure(string $serviceId): ClosureReferenceConfigurator +{ + return new ClosureReferenceConfigurator($serviceId); +} + +/** + * Creates a closure. + */ +function closure(string|array|\Closure|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator +{ + return (new InlineServiceConfigurator(new Definition('Closure'))) + ->factory(['Closure', 'fromCallable']) + ->args([$callable]); +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php index 22a681416c94c..d4364a51a4b2d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/AbstractConfigurator.php @@ -101,6 +101,9 @@ public static function processValue(mixed $value, bool $allowServices = false): case $value instanceof \UnitEnum: return $value; + case $value instanceof \Closure: + return self::processClosure($value); + case $value instanceof ArgumentInterface: case $value instanceof Definition: case $value instanceof Expression: @@ -120,7 +123,7 @@ public static function processValue(mixed $value, bool $allowServices = false): * * @throws InvalidArgumentException if the closure is anonymous or references a non-static method */ - final public static function processClosure(\Closure $closure): callable + private static function processClosure(\Closure $closure): callable { $function = new \ReflectionFunction($closure); if ($function->isAnonymous()) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php index e9bbe6036afc2..9a8188f3f3bf6 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php @@ -11,18 +11,13 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -use Symfony\Component\Config\Loader\ParamConfigurator; -use Symfony\Component\DependencyInjection\Argument\AbstractArgument; -use Symfony\Component\DependencyInjection\Argument\IteratorArgument; -use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; -use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Loader\UndefinedExtensionHandler; -use Symfony\Component\ExpressionLanguage\Expression; + +require_once __DIR__.\DIRECTORY_SEPARATOR.'functions.php'; /** * @author Nicolas Grekas @@ -94,111 +89,3 @@ final public function withPath(string $path): static return $clone; } } - -/** - * Creates a parameter. - */ -function param(string $name): ParamConfigurator -{ - return new ParamConfigurator($name); -} - -/** - * Creates a reference to a service. - */ -function service(string $serviceId): ReferenceConfigurator -{ - return new ReferenceConfigurator($serviceId); -} - -/** - * Creates an inline service. - */ -function inline_service(?string $class = null): InlineServiceConfigurator -{ - return new InlineServiceConfigurator(new Definition($class)); -} - -/** - * Creates a service locator. - * - * @param array $values - */ -function service_locator(array $values): ServiceLocatorArgument -{ - $values = AbstractConfigurator::processValue($values, true); - - return new ServiceLocatorArgument($values); -} - -/** - * Creates a lazy iterator. - * - * @param ReferenceConfigurator[] $values - */ -function iterator(array $values): IteratorArgument -{ - return new IteratorArgument(AbstractConfigurator::processValue($values, true)); -} - -/** - * Creates a lazy iterator by tag name. - */ -function tagged_iterator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): TaggedIteratorArgument -{ - return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf); -} - -/** - * Creates a service locator by tag name. - */ -function tagged_locator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): ServiceLocatorArgument -{ - return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)); -} - -/** - * Creates an expression. - */ -function expr(string $expression): Expression -{ - return new Expression($expression); -} - -/** - * Creates an abstract argument. - */ -function abstract_arg(string $description): AbstractArgument -{ - return new AbstractArgument($description); -} - -/** - * Creates an environment variable reference. - */ -function env(string $name): EnvConfigurator -{ - return new EnvConfigurator($name); -} - -/** - * Creates a closure service reference. - */ -function service_closure(string $serviceId): ClosureReferenceConfigurator -{ - return new ClosureReferenceConfigurator($serviceId); -} - -/** - * Creates a closure. - */ -function closure(string|array|\Closure|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator -{ - if ($callable instanceof \Closure) { - $callable = AbstractConfigurator::processClosure($callable); - } - - return (new InlineServiceConfigurator(new Definition('Closure'))) - ->factory(['Closure', 'fromCallable']) - ->args([$callable]); -} diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/ConfiguratorTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/ConfiguratorTrait.php index b88fda6771801..5d1a31f6a30ce 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/ConfiguratorTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/ConfiguratorTrait.php @@ -22,12 +22,6 @@ trait ConfiguratorTrait */ final public function configurator(string|array|\Closure|ReferenceConfigurator $configurator): static { - if ($configurator instanceof \Closure) { - $this->definition->setConfigurator(static::processClosure($configurator)); - - return $this; - } - $this->definition->setConfigurator(static::processValue($configurator, true)); return $this; diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php index 72a315497637d..4b6a6243aa003 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php @@ -24,12 +24,6 @@ trait FactoryTrait */ final public function factory(string|array|\Closure|ReferenceConfigurator|Expression $factory): static { - if ($factory instanceof \Closure) { - $this->definition->setFactory(static::processClosure($factory)); - - return $this; - } - if (\is_string($factory) && 1 === substr_count($factory, ':')) { $factoryParts = explode(':', $factory); diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php index 7865883314a2a..c48f3d1ec20ba 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FromCallableTrait.php @@ -41,10 +41,6 @@ final public function fromCallable(string|array|\Closure|ReferenceConfigurator|E $this->definition->setFactory(['Closure', 'fromCallable']); - if ($callable instanceof \Closure) { - $callable = static::processClosure($callable); - } - if (\is_string($callable) && 1 === substr_count($callable, ':')) { $parts = explode(':', $callable); diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/functions.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/functions.php new file mode 100644 index 0000000000000..0779d25e037aa --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/functions.php @@ -0,0 +1,124 @@ + + * + * 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\Config\Loader\ParamConfigurator; +use Symfony\Component\DependencyInjection\Argument\AbstractArgument; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\ExpressionLanguage\Expression; + +/** + * Creates a parameter. + */ +function param(string $name): ParamConfigurator +{ + return new ParamConfigurator($name); +} + +/** + * Creates a reference to a service. + */ +function service(string $serviceId): ReferenceConfigurator +{ + return new ReferenceConfigurator($serviceId); +} + +/** + * Creates an inline service. + */ +function inline_service(?string $class = null): InlineServiceConfigurator +{ + return new InlineServiceConfigurator(new Definition($class)); +} + +/** + * Creates a service locator. + * + * @param array $values + */ +function service_locator(array $values): ServiceLocatorArgument +{ + $values = AbstractConfigurator::processValue($values, true); + + return new ServiceLocatorArgument($values); +} + +/** + * Creates a lazy iterator. + * + * @param ReferenceConfigurator[] $values + */ +function iterator(array $values): IteratorArgument +{ + return new IteratorArgument(AbstractConfigurator::processValue($values, true)); +} + +/** + * Creates a lazy iterator by tag name. + */ +function tagged_iterator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): TaggedIteratorArgument +{ + return new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf); +} + +/** + * Creates a service locator by tag name. + */ +function tagged_locator(string $tag, ?string $indexAttribute = null, ?string $defaultIndexMethod = null, ?string $defaultPriorityMethod = null, string|array $exclude = [], bool $excludeSelf = true): ServiceLocatorArgument +{ + return new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)); +} + +/** + * Creates an expression. + */ +function expr(string $expression): Expression +{ + return new Expression($expression); +} + +/** + * Creates an abstract argument. + */ +function abstract_arg(string $description): AbstractArgument +{ + return new AbstractArgument($description); +} + +/** + * Creates an environment variable reference. + */ +function env(string $name): EnvConfigurator +{ + return new EnvConfigurator($name); +} + +/** + * Creates a closure service reference. + */ +function service_closure(string $serviceId): ClosureReferenceConfigurator +{ + return new ClosureReferenceConfigurator($serviceId); +} + +/** + * Creates a closure. + */ +function closure(string|array|\Closure|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator +{ + return (new InlineServiceConfigurator(new Definition('Closure'))) + ->factory(['Closure', 'fromCallable']) + ->args([$callable]); +} diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 51b2980b56e70..1b96a7e77d150 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -261,23 +261,6 @@ public function registerAliasesForSinglyImplementedInterfaces(): void final protected function loadExtensionConfig(string $namespace, array $config, string $file = '?'): void { - if (\in_array($namespace, ['imports', 'services', 'parameters'], true)) { - $yamlLoader = new YamlFileLoader($this->container, $this->locator, $this->env, $this->prepend); - $loadContent = new \ReflectionMethod(YamlFileLoader::class, 'loadContent'); - $loadContent->invoke($yamlLoader, [$namespace => $config], $file); - - if ($this->env && isset($config['when@'.$this->env])) { - if (!\is_array($config['when@'.$this->env])) { - throw new InvalidArgumentException(\sprintf('The "when@%s" key should contain an array in "%s".', $this->env, $file)); - } - - $yamlLoader->env = null; - $loadContent->invoke($yamlLoader, [$namespace => $config['when@'.$this->env]], $file); - } - - return; - } - if (!$this->prepend) { $this->container->loadFromExtension($namespace, $config); diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 18abe969f1dc9..0646502e2d29e 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -24,6 +24,7 @@ use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Config\ServicesConfig; /** * PhpFileLoader loads service definitions from a PHP file. @@ -58,8 +59,9 @@ public function load(mixed $resource, ?string $type = null): mixed $this->setCurrentDir(\dirname($path)); $this->container->fileExists($path); - // Force load ContainerConfigurator to make env(), param() etc available. - class_exists(ContainerConfigurator::class); + // Ensure symbols in the \Symfony\Config and Configurator namespaces are available + require_once __DIR__.\DIRECTORY_SEPARATOR.'Config'.\DIRECTORY_SEPARATOR.'functions.php'; + require_once __DIR__.\DIRECTORY_SEPARATOR.'Configurator'.\DIRECTORY_SEPARATOR.'functions.php'; if ($autoloaderRegistered = !$this->configBuilderAutoloader && $this->generator) { spl_autoload_register($this->configBuilderAutoloader = function (string $class) { @@ -94,23 +96,48 @@ class_exists(ContainerConfigurator::class); if (\is_object($result) && \is_callable($result)) { $result = $this->callConfigurator($result, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path); } - if ($result instanceof ConfigBuilderInterface) { - $this->loadExtensionConfig($result->getExtensionAlias(), ContainerConfigurator::processValue($result->toArray()), $path); - } elseif (is_iterable($result)) { - foreach ($result as $key => $config) { - if ($config instanceof ConfigBuilderInterface) { + if ($result instanceof ConfigBuilderInterface || $result instanceof ServicesConfig) { + $result = [$result]; + } elseif (!is_iterable($result ?? [])) { + throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($result))); + } + + foreach ($result ?? [] as $key => $config) { + if (!str_starts_with($key, 'when@')) { + $config = [$key => $config]; + } elseif (!$this->env || 'when@'.$this->env !== $key) { + continue; + } elseif ($config instanceof ServicesConfig || $config instanceof ConfigBuilderInterface) { + $config = [$config]; + } elseif (!is_iterable($config)) { + throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $key, $path)); + } + + foreach ($config as $key => $config) { + if ($config instanceof ServicesConfig || \in_array($key, ['imports', 'parameters', 'services'], true)) { + if (!$config instanceof ServicesConfig) { + $config = [$key => $config]; + } elseif (\is_string($key) && 'services' !== $key) { + throw new InvalidArgumentException(\sprintf('Invalid key "%s" returned for the "%s" config builder; none or "services" expected in file "%s".', $key, get_debug_type($config), $path)); + } + $yamlLoader = new YamlFileLoader($this->container, $this->locator, $this->env, $this->prepend); + $loadContent = new \ReflectionMethod(YamlFileLoader::class, 'loadContent'); + $loadContent->invoke($yamlLoader, ContainerConfigurator::processValue((array) $config), $path); + } elseif ($config instanceof ConfigBuilderInterface) { if (\is_string($key) && $config->getExtensionAlias() !== $key) { throw new InvalidArgumentException(\sprintf('The extension alias "%s" of the "%s" config builder does not match the key "%s" in file "%s".', $config->getExtensionAlias(), get_debug_type($config), $key, $path)); } $this->loadExtensionConfig($config->getExtensionAlias(), ContainerConfigurator::processValue($config->toArray()), $path); } elseif (!\is_string($key) || !\is_array($config)) { - throw new InvalidArgumentException(\sprintf('The configuration returned in file "%s" must yield only string-keyed arrays or ConfigBuilderInterface values.', $path)); + throw new InvalidArgumentException(\sprintf('The configuration returned in file "%s" must yield only string-keyed arrays or ConfigBuilderInterface objects.', $path)); } else { + if (str_starts_with($key, 'when@')) { + throw new InvalidArgumentException(\sprintf('A service name cannot start with "when@" in "%s".', $path)); + } + $this->loadExtensionConfig($key, ContainerConfigurator::processValue($config), $path); } } - } elseif (null !== $result) { - throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($result))); } $this->loadExtensionConfigs(); @@ -254,6 +281,10 @@ private function configBuilder(string $namespace): ConfigBuilderInterface throw new InvalidArgumentException(\sprintf('Could not find or generate class "%s".', $namespace)); } + if (is_a($namespace, ServicesConfig::class, true)) { + throw new \LogicException(\sprintf('You cannot use "%s" as a config builder; create an instance and return it instead.', $namespace)); + } + // Try to get the extension alias $alias = Container::underscore(substr($namespace, 15, -6)); diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 51f449a23b7dc..839f294ea8b3b 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -655,17 +655,13 @@ private function parseDefinition(string $id, array|string|null $service, string } $decorationOnInvalid = \array_key_exists('decoration_on_invalid', $service) ? $service['decoration_on_invalid'] : 'exception'; - if ('exception' === $decorationOnInvalid) { - $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; - } elseif ('ignore' === $decorationOnInvalid) { - $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; - } elseif (null === $decorationOnInvalid) { - $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; - } elseif ('null' === $decorationOnInvalid) { - throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean null (without quotes) in "%s"?', $decorationOnInvalid, $id, $file)); - } else { - throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean "exception", "ignore" or null in "%s"?', $decorationOnInvalid, $id, $file)); - } + $invalidBehavior = match ($decorationOnInvalid) { + 'exception', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE => ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, + 'ignore', ContainerInterface::IGNORE_ON_INVALID_REFERENCE => ContainerInterface::IGNORE_ON_INVALID_REFERENCE, + null, ContainerInterface::NULL_ON_INVALID_REFERENCE => ContainerInterface::NULL_ON_INVALID_REFERENCE, + 'null' => throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean null (without quotes) in "%s"?', $decorationOnInvalid, $id, $file)), + default => throw new InvalidArgumentException(\sprintf('Invalid value "%s" for attribute "decoration_on_invalid" on service "%s". Did you mean "exception", "ignore" or "null" in "%s"?', $decorationOnInvalid, $id, $file)), + }; $renameId = $service['decoration_inner_name'] ?? null; $priority = $service['decoration_priority'] ?? 0; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/object_array_config.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/object_array_config.expected.yml new file mode 100644 index 0000000000000..925bc1fd39189 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/object_array_config.expected.yml @@ -0,0 +1,15 @@ +parameters: + foo: bar + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar + public: true + my_service: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar + public: true + arguments: [bar] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/object_array_config.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/object_array_config.php new file mode 100644 index 0000000000000..821154be5d66e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/object_array_config.php @@ -0,0 +1,19 @@ + 'bar', + ], + defaults: [ + 'public' => true, + ], + services: [ + Bar::class => null, + 'my_service' => [ + 'class' => Bar::class, + 'arguments' => ['%foo%'], + ], +]); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_when_env.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_when_env.expected.yml new file mode 100644 index 0000000000000..18616d525c74e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_when_env.expected.yml @@ -0,0 +1,15 @@ +parameters: + foo_param: bar_value + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar + public: true + my_service: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar + public: true + arguments: [bar_value] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_when_env.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_when_env.php new file mode 100644 index 0000000000000..bd0a80896ed5c --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_when_env.php @@ -0,0 +1,24 @@ + [ + 'parameters' => [ + 'foo_param' => 'bar_value', + ], + new ServicesConfig( + defaults: [ + 'public' => true, + ], + services: [ + Bar::class => null, + 'my_service' => [ + 'class' => Bar::class, + 'arguments' => ['%foo_param%'], + ], + ] + ), + ], +]; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/Configurator/AbstractConfiguratorTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/Configurator/AbstractConfiguratorTest.php index fb639db072196..2d0d7c000e6a6 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/Configurator/AbstractConfiguratorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/Configurator/AbstractConfiguratorTest.php @@ -21,12 +21,12 @@ public function testProcessClosure() { $this->assertSame( [\DateTime::class, 'createFromFormat'], - AbstractConfigurator::processClosure(\DateTime::createFromFormat(...)), + AbstractConfigurator::processValue(\DateTime::createFromFormat(...)), ); $this->assertSame( 'date_create', - AbstractConfigurator::processClosure(date_create(...)), + AbstractConfigurator::processValue(date_create(...)), ); } @@ -35,7 +35,7 @@ public function testProcessNonStaticNamedClosure() self::expectException(InvalidArgumentException::class); self::expectExceptionMessage('The method "DateTime::format(...)" is not static'); - AbstractConfigurator::processClosure((new \DateTime())->format(...)); + AbstractConfigurator::processValue((new \DateTime())->format(...)); } public function testProcessAnonymousClosure() @@ -43,6 +43,6 @@ public function testProcessAnonymousClosure() self::expectException(InvalidArgumentException::class); self::expectExceptionMessage('Anonymous closure not supported. The closure must be created from a static method or a global function.'); - AbstractConfigurator::processClosure(static fn () => new \DateTime()); + AbstractConfigurator::processValue(static fn () => new \DateTime()); } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index e71728517877e..c0b74b90f07e7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -31,6 +31,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; +use Symfony\Config\ServicesConfig; class PhpFileLoaderTest extends TestCase { @@ -50,6 +51,16 @@ public function testLoad() $loader->load(__DIR__.'/../Fixtures/php/simple.php'); $this->assertEquals('foo', $container->getParameter('foo'), '->load() loads a PHP file resource'); + + $this->assertTrue(class_exists(ServicesConfig::class)); + $this->assertTrue(\function_exists('Symfony\Config\service')); + $this->assertTrue(\function_exists('Symfony\Component\DependencyInjection\Loader\Configurator\service')); + + $configCode = explode("\n/**", file_get_contents(\dirname(__DIR__, 2).'/Loader/Config/functions.php'), 2); + $configuratorCode = explode("\n/**", file_get_contents(\dirname(__DIR__, 2).'/Loader/Configurator/functions.php'), 2); + + $this->assertStringEqualsFile(\dirname(__DIR__, 2).'/Loader/Config/functions.php', $configCode[0]."\n/**".$configuratorCode[1]); + $this->assertStringEqualsFile(\dirname(__DIR__, 2).'/Loader/Configurator/functions.php', $configuratorCode[0]."\n/**".$configCode[1]); } public function testPrependExtensionConfigWithLoadMethod() @@ -145,6 +156,8 @@ public static function provideConfig() yield ['from_callable']; yield ['env_param']; yield ['array_config']; + yield ['object_array_config']; + yield ['return_when_env']; } public function testResourceTags() diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 0a6963357b094..e79d577cba246 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -38,7 +38,10 @@ "symfony/service-implementation": "1.1|2.0|3.0" }, "autoload": { - "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" }, + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "", + "Symfony\\Config\\": "Loader/Config/" + }, "exclude-from-classmap": [ "/Tests/" ] diff --git a/src/Symfony/Component/Routing/CHANGELOG.md b/src/Symfony/Component/Routing/CHANGELOG.md index 351680a2a48a6..cc2f7ec1ac669 100644 --- a/src/Symfony/Component/Routing/CHANGELOG.md +++ b/src/Symfony/Component/Routing/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add support of multiple env names in the `Symfony\Component\Routing\Attribute\Route` attribute * Add argument `$parameters` to `RequestContext`'s constructor * Handle declaring routes using PHP arrays that follow the same shape as corresponding yaml files + * Add `RoutesConfig` to help writing PHP configs using yaml-like array-shapes * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters and setters in attribute classes in favor of public properties * Deprecate accessing the internal scope of the loader in PHP config files, use only its public API instead diff --git a/src/Symfony/Component/Routing/Loader/Config/RoutesConfig.php b/src/Symfony/Component/Routing/Loader/Config/RoutesConfig.php new file mode 100644 index 0000000000000..7d482e36da08e --- /dev/null +++ b/src/Symfony/Component/Routing/Loader/Config/RoutesConfig.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Config; + +/** + * @psalm-type Route = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type Import = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type Alias = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type Routes = array + */ +class RoutesConfig +{ + /** + * @param Routes $routes + */ + public function __construct( + public readonly array $routes, + ) { + } +} diff --git a/src/Symfony/Component/Routing/Loader/PhpFileLoader.php b/src/Symfony/Component/Routing/Loader/PhpFileLoader.php index 9e005c83de493..5fe3efac66b02 100644 --- a/src/Symfony/Component/Routing/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/PhpFileLoader.php @@ -13,12 +13,14 @@ use Symfony\Component\Config\Loader\FileLoader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Exception\InvalidArgumentException; use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; use Symfony\Component\Routing\Loader\Configurator\ImportConfigurator; use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\RouteCollection; +use Symfony\Config\RoutesConfig; /** * PhpFileLoader loads routes from a PHP file. @@ -105,38 +107,45 @@ private function loadRoutes(RouteCollection $collection, mixed $routes, string $ return; } - if (!is_iterable($routes)) { - throw new \InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); + if ($routes instanceof RoutesConfig) { + $routes = [$routes]; + } elseif (!is_iterable($routes)) { + throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid: "%s" given.', $path, get_debug_type($routes))); } $loader = new YamlFileLoader($this->locator, $this->env); \Closure::bind(function () use ($collection, $routes, $path, $file) { foreach ($routes as $name => $config) { + $when = $name; if (str_starts_with($name, 'when@')) { if (!$this->env || 'when@'.$this->env !== $name) { continue; } + $when .= '" when "@'.$this->env; + } elseif (!$config instanceof RoutesConfig) { + $config = [$name => $config]; + } elseif (!\is_int($name)) { + throw new InvalidArgumentException(\sprintf('Invalid key "%s" returned for the "%s" config builder; none or "when@%%env%%" expected in file "%s".', $name, get_debug_type($config), $path)); + } - foreach ($config as $name => $config) { - $this->validate($config, $name.'" when "@'.$this->env, $path); - - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); - } - } - - continue; + if ($config instanceof RoutesConfig) { + $config = $config->routes; + } elseif (!is_iterable($config)) { + throw new InvalidArgumentException(\sprintf('The "%s" key should contain an array in "%s".', $name, $path)); } - $this->validate($config, $name, $path); + foreach ($config as $name => $config) { + if (str_starts_with($name, 'when@')) { + throw new InvalidArgumentException(\sprintf('A route name cannot start with "when@" in "%s".', $path)); + } + $this->validate($config, $when, $path); - if (isset($config['resource'])) { - $this->parseImport($collection, $config, $path, $file); - } else { - $this->parseRoute($collection, $name, $config, $path); + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } } } }, $loader, $loader::class)(); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/routes_object.php b/src/Symfony/Component/Routing/Tests/Fixtures/routes_object.php new file mode 100644 index 0000000000000..440633e9bc0d8 --- /dev/null +++ b/src/Symfony/Component/Routing/Tests/Fixtures/routes_object.php @@ -0,0 +1,13 @@ + [ + 'path' => '/a', + ], + 'b' => [ + 'path' => '/b', + 'methods' => ['GET'], + ], +]); diff --git a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php index e033d7b718627..73dae76754359 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php @@ -374,6 +374,15 @@ public function testLoadsArrayRoutes() $this->assertSame(['GET'], $routes->get('b')->getMethods()); } + public function testLoadsObjectRoutes() + { + $loader = new PhpFileLoader(new FileLocator([__DIR__.'/../Fixtures'])); + $routes = $loader->load('routes_object.php'); + $this->assertSame('/a', $routes->get('a')->getPath()); + $this->assertSame('/b', $routes->get('b')->getPath()); + $this->assertSame(['GET'], $routes->get('b')->getMethods()); + } + public function testWhenEnvWithArray() { $locator = new FileLocator([__DIR__.'/../Fixtures']); diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 1fcc24b61606c..3ea442bbe8c56 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -33,7 +33,10 @@ "symfony/yaml": "<6.4" }, "autoload": { - "psr-4": { "Symfony\\Component\\Routing\\": "" }, + "psr-4": { + "Symfony\\Component\\Routing\\": "", + "Symfony\\Config\\": "Loader/Config/" + }, "exclude-from-classmap": [ "/Tests/" ]