diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index d10b81f164495..ffb278bd030e3 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG * Allow using environment variables in `EnumNode` * Add Node's information in generated Config + * Add `DefinitionFileLoader` class to load a TreeBuilder definition from an external file + * Add `DefinitionConfigurator` helper 6.0 --- diff --git a/src/Symfony/Component/Config/Definition/ConfigurableInterface.php b/src/Symfony/Component/Config/Definition/ConfigurableInterface.php new file mode 100644 index 0000000000000..cd4646160ab8e --- /dev/null +++ b/src/Symfony/Component/Config/Definition/ConfigurableInterface.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\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; + +/** + * @author Yonel Ceruto + */ +interface ConfigurableInterface +{ + /** + * Generates the configuration tree builder. + */ + public function configure(DefinitionConfigurator $definition): void; +} diff --git a/src/Symfony/Component/Config/Definition/Configuration.php b/src/Symfony/Component/Config/Definition/Configuration.php new file mode 100644 index 0000000000000..32954a66a9767 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Configuration.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\Config\Definition\Loader\DefinitionFileLoader; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Yonel Ceruto + * + * @final + */ +class Configuration implements ConfigurationInterface +{ + public function __construct( + private ConfigurableInterface $subject, + private ?ContainerBuilder $container, + private string $alias, + ) { + } + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder($this->alias); + $file = (new \ReflectionObject($this->subject))->getFileName(); + $loader = new DefinitionFileLoader($treeBuilder, new FileLocator(\dirname($file)), $this->container); + $configurator = new DefinitionConfigurator($treeBuilder, $loader, $file, $file); + + $this->subject->configure($configurator); + + return $treeBuilder; + } +} diff --git a/src/Symfony/Component/Config/Definition/Configurator/DefinitionConfigurator.php b/src/Symfony/Component/Config/Definition/Configurator/DefinitionConfigurator.php new file mode 100644 index 0000000000000..006a444bedcb0 --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Configurator/DefinitionConfigurator.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Configurator; + +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\Loader\DefinitionFileLoader; + +/** + * @author Yonel Ceruto + */ +class DefinitionConfigurator +{ + public function __construct( + private TreeBuilder $treeBuilder, + private DefinitionFileLoader $loader, + private string $path, + private string $file, + ) { + } + + public function import(string $resource, string $type = null, bool $ignoreErrors = false): void + { + $this->loader->setCurrentDir(\dirname($this->path)); + $this->loader->import($resource, $type, $ignoreErrors, $this->file); + } + + public function rootNode(): NodeDefinition|ArrayNodeDefinition + { + return $this->treeBuilder->getRootNode(); + } + + public function setPathSeparator(string $separator): void + { + $this->treeBuilder->setPathSeparator($separator); + } +} diff --git a/src/Symfony/Component/Config/Definition/Loader/DefinitionFileLoader.php b/src/Symfony/Component/Config/Definition/Loader/DefinitionFileLoader.php new file mode 100644 index 0000000000000..1a1a37d041efd --- /dev/null +++ b/src/Symfony/Component/Config/Definition/Loader/DefinitionFileLoader.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Definition\Loader; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * DefinitionFileLoader loads config definitions from a PHP file. + * + * The PHP file is required. + * + * @author Yonel Ceruto + */ +class DefinitionFileLoader extends FileLoader +{ + public function __construct( + private TreeBuilder $treeBuilder, + FileLocatorInterface $locator, + private ?ContainerBuilder $container = null, + ) { + parent::__construct($locator); + } + + /** + * {@inheritdoc} + */ + public function load(mixed $resource, string $type = null): mixed + { + // the loader variable is exposed to the included file below + $loader = $this; + + $path = $this->locator->locate($resource); + $this->setCurrentDir(\dirname($path)); + $this->container?->fileExists($path); + + // the closure forbids access to the private scope in the included file + $load = \Closure::bind(static function ($file) use ($loader) { + return include $file; + }, null, ProtectedDefinitionFileLoader::class); + + $callback = $load($path); + + if (\is_object($callback) && \is_callable($callback)) { + $this->executeCallback($callback, new DefinitionConfigurator($this->treeBuilder, $this, $path, $resource), $path); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function supports(mixed $resource, string $type = null): bool + { + if (!\is_string($resource)) { + return false; + } + + if (null === $type && 'php' === pathinfo($resource, \PATHINFO_EXTENSION)) { + return true; + } + + return 'php' === $type; + } + + private function executeCallback(callable $callback, DefinitionConfigurator $configurator, string $path): void + { + $callback = $callback(...); + + $arguments = []; + $r = new \ReflectionFunction($callback); + + foreach ($r->getParameters() as $parameter) { + $reflectionType = $parameter->getType(); + + if (!$reflectionType instanceof \ReflectionNamedType) { + throw new \InvalidArgumentException(sprintf('Could not resolve argument "$%s" for "%s". You must typehint it (for example with "%s").', $parameter->getName(), $path, DefinitionConfigurator::class)); + } + + $arguments[] = match ($reflectionType->getName()) { + DefinitionConfigurator::class => $configurator, + TreeBuilder::class => $this->treeBuilder, + FileLoader::class, self::class => $this, + }; + } + + $callback(...$arguments); + } +} + +/** + * @internal + */ +final class ProtectedDefinitionFileLoader extends DefinitionFileLoader +{ +} diff --git a/src/Symfony/Component/Config/Tests/Definition/Loader/DefinitionFileLoaderTest.php b/src/Symfony/Component/Config/Tests/Definition/Loader/DefinitionFileLoaderTest.php new file mode 100644 index 0000000000000..e16f329f532da --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Definition/Loader/DefinitionFileLoaderTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Definition\Loader; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\BaseNode; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\Loader\DefinitionFileLoader; +use Symfony\Component\Config\FileLocator; + +class DefinitionFileLoaderTest extends TestCase +{ + public function testSupports() + { + $loader = new DefinitionFileLoader(new TreeBuilder('test'), new FileLocator()); + + $this->assertTrue($loader->supports('foo.php'), '->supports() returns true if the resource is loadable'); + $this->assertFalse($loader->supports('foo.foo'), '->supports() returns false if the resource is not loadable'); + $this->assertTrue($loader->supports('with_wrong_ext.yml', 'php'), '->supports() returns true if the resource with forced type is loadable'); + } + + public function testLoad() + { + $loader = new DefinitionFileLoader($treeBuilder = new TreeBuilder('test'), new FileLocator()); + $loader->load(__DIR__.'/../../Fixtures/Loader/node_simple.php'); + + $children = $treeBuilder->buildTree()->getChildren(); + + $this->assertArrayHasKey('foo', $children); + $this->assertInstanceOf(BaseNode::class, $children['foo']); + $this->assertSame('test.foo', $children['foo']->getPath(), '->load() loads a PHP file resource'); + } +} diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Loader/node_simple.php b/src/Symfony/Component/Config/Tests/Fixtures/Loader/node_simple.php new file mode 100644 index 0000000000000..c7b3e5d31de2e --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Fixtures/Loader/node_simple.php @@ -0,0 +1,10 @@ +getRootNode() + ->children() + ->scalarNode('foo')->end() + ->end(); +}; diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index f798f11897d5b..b2ac3e0456ba6 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add an `Autowire` attribute to tell a parameter how to be autowired * Allow using expressions as service factories * Deprecate `ReferenceSetArgumentTrait` + * Add `AbstractExtension` class for DI configuration/definition on a single file 6.0 --- diff --git a/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php b/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php new file mode 100644 index 0000000000000..c5c2f17adf975 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.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\Component\DependencyInjection\Extension; + +use Symfony\Component\Config\Definition\Configuration; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +/** + * An Extension that provides configuration hooks. + * + * @author Yonel Ceruto + */ +abstract class AbstractExtension extends Extension implements ConfigurableExtensionInterface, PrependExtensionInterface +{ + use ExtensionTrait; + + public function configure(DefinitionConfigurator $definition): void + { + } + + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + } + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return new Configuration($this, $container, $this->getAlias()); + } + + final public function prepend(ContainerBuilder $container): void + { + $callback = function (ContainerConfigurator $configurator) use ($container) { + $this->prependExtension($configurator, $container); + }; + + $this->executeConfiguratorCallback($container, $callback, $this); + } + + final public function load(array $configs, ContainerBuilder $container): void + { + $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); + + $callback = function (ContainerConfigurator $configurator) use ($config, $container) { + $this->loadExtension($config, $configurator, $container); + }; + + $this->executeConfiguratorCallback($container, $callback, $this); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Extension/ConfigurableExtensionInterface.php b/src/Symfony/Component/DependencyInjection/Extension/ConfigurableExtensionInterface.php new file mode 100644 index 0000000000000..4a35005a744d9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Extension/ConfigurableExtensionInterface.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\Component\DependencyInjection\Extension; + +use Symfony\Component\Config\Definition\ConfigurableInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +/** + * @author Yonel Ceruto + */ +interface ConfigurableExtensionInterface extends ConfigurableInterface +{ + /** + * Allow an extension to prepend the extension configurations. + */ + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void; + + /** + * Loads a specific configuration. + */ + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void; +} diff --git a/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php b/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php new file mode 100644 index 0000000000000..d920b848a9118 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Extension; + +use Symfony\Component\Config\Builder\ConfigBuilderGenerator; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\DelegatingLoader; +use Symfony\Component\Config\Loader\LoaderResolver; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\ClosureLoader; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; +use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; +use Symfony\Component\DependencyInjection\Loader\IniFileLoader; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; + +/** + * @author Yonel Ceruto + */ +trait ExtensionTrait +{ + private function executeConfiguratorCallback(ContainerBuilder $container, \Closure $callback, ConfigurableExtensionInterface $subject): void + { + $env = $container->getParameter('kernel.environment'); + $loader = $this->createContainerLoader($container, $env); + $file = (new \ReflectionObject($subject))->getFileName(); + $bundleLoader = $loader->getResolver()->resolve($file); + if (!$bundleLoader instanceof PhpFileLoader) { + throw new \LogicException('Unable to create the ContainerConfigurator.'); + } + $bundleLoader->setCurrentDir(\dirname($file)); + $instanceof = &\Closure::bind(function &() { return $this->instanceof; }, $bundleLoader, $bundleLoader)(); + + try { + $callback(new ContainerConfigurator($container, $bundleLoader, $instanceof, $file, $file, $env)); + } finally { + $instanceof = []; + $bundleLoader->registerAliasesForSinglyImplementedInterfaces(); + } + } + + private function createContainerLoader(ContainerBuilder $container, string $env): DelegatingLoader + { + $buildDir = $container->getParameter('kernel.build_dir'); + $locator = new FileLocator(); + $resolver = new LoaderResolver([ + new XmlFileLoader($container, $locator, $env), + new YamlFileLoader($container, $locator, $env), + new IniFileLoader($container, $locator, $env), + new PhpFileLoader($container, $locator, $env, new ConfigBuilderGenerator($buildDir)), + new GlobFileLoader($container, $locator, $env), + new DirectoryLoader($container, $locator, $env), + new ClosureLoader($container, $env), + ]); + + return new DelegatingLoader($resolver); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php b/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php new file mode 100644 index 0000000000000..cb56983d365ce --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Extension; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Config\Definition\ConfigurableInterface; +use Symfony\Component\Config\Definition\Configuration; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\AbstractExtension; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; + +class AbstractExtensionTest extends TestCase +{ + public function testConfiguration() + { + $extension = new class() extends AbstractExtension { + public function configure(DefinitionConfigurator $definition): void + { + // load one + $definition->import('../Fixtures/config/definition/foo.php'); + + // load multiples + $definition->import('../Fixtures/config/definition/multiple/*.php'); + + // inline + $definition->rootNode() + ->children() + ->scalarNode('ping')->defaultValue('inline')->end() + ->end(); + } + }; + + $expected = [ + 'foo' => 'one', + 'bar' => 'multi', + 'baz' => 'multi', + 'ping' => 'inline', + ]; + + self::assertSame($expected, $this->processConfiguration($extension)); + } + + public function testPrependAppendExtensionConfig() + { + $extension = new class() extends AbstractExtension { + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + // append config + $container->extension('third', ['foo' => 'append']); + + // prepend config + $builder->prependExtensionConfig('third', ['foo' => 'prepend']); + } + }; + + $container = $this->processPrependExtension($extension); + + $expected = [ + ['foo' => 'prepend'], + ['foo' => 'bar'], + ['foo' => 'append'], + ]; + + self::assertSame($expected, $container->getExtensionConfig('third')); + } + + public function testLoadExtension() + { + $extension = new class() extends AbstractExtension { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../Fixtures/config/definition/foo.php'); + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->parameters() + ->set('foo_param', $config) + ; + + $container->services() + ->set('foo_service', \stdClass::class) + ; + + $container->import('../Fixtures/config/services.php'); + } + + public function getAlias(): string + { + return 'micro'; + } + }; + + $container = $this->processLoadExtension($extension, [['foo' => 'bar']]); + + self::assertSame(['foo' => 'bar'], $container->getParameter('foo_param')); + self::assertTrue($container->hasDefinition('foo_service')); + self::assertTrue($container->hasDefinition('bar_service')); + } + + protected function processConfiguration(ConfigurableInterface $configurable): array + { + $configuration = new Configuration($configurable, null, 'micro'); + + return (new Processor())->process($configuration->getConfigTreeBuilder()->buildTree(), []); + } + + protected function processPrependExtension(PrependExtensionInterface $extension): ContainerBuilder + { + $thirdExtension = new class() extends AbstractExtension { + public function configure(DefinitionConfigurator $definition): void + { + $definition->import('../Fixtures/config/definition/foo.php'); + } + + public function getAlias(): string + { + return 'third'; + } + }; + + $container = $this->createContainerBuilder(); + $container->registerExtension($thirdExtension); + $container->loadFromExtension('third', ['foo' => 'bar']); + + $extension->prepend($container); + + return $container; + } + + protected function processLoadExtension(ExtensionInterface $extension, array $configs): ContainerBuilder + { + $container = $this->createContainerBuilder(); + + $extension->load($configs, $container); + + return $container; + } + + protected function createContainerBuilder(): ContainerBuilder + { + return new ContainerBuilder(new ParameterBag([ + 'kernel.environment' => 'test', + 'kernel.build_dir' => 'test', + ])); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/foo.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/foo.php new file mode 100644 index 0000000000000..9602c80c78ab3 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/foo.php @@ -0,0 +1,11 @@ +rootNode() + ->children() + ->scalarNode('foo')->defaultValue('one')->end() + ->end() + ; +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/multiple/bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/multiple/bar.php new file mode 100644 index 0000000000000..82b2ac60d41bb --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/multiple/bar.php @@ -0,0 +1,11 @@ +rootNode() + ->children() + ->scalarNode('bar')->defaultValue('multi')->end() + ->end() + ; +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/multiple/baz.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/multiple/baz.php new file mode 100644 index 0000000000000..4efc58dae4eb9 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/definition/multiple/baz.php @@ -0,0 +1,11 @@ +rootNode() + ->children() + ->scalarNode('baz')->defaultValue('multi')->end() + ->end() + ; +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/services.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/services.php new file mode 100644 index 0000000000000..200ec62f7df98 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/services.php @@ -0,0 +1,9 @@ +services() + ->set('bar_service', stdClass::class) + ; +}; diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 1b2e7850c8c92..69e68486775fa 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -23,7 +23,7 @@ }, "require-dev": { "symfony/yaml": "^5.4|^6.0", - "symfony/config": "^5.4|^6.0", + "symfony/config": "^6.1", "symfony/expression-language": "^5.4|^6.0" }, "suggest": { @@ -35,7 +35,7 @@ }, "conflict": { "ext-psr": "<1.1|>=2", - "symfony/config": "<5.4", + "symfony/config": "<6.1", "symfony/finder": "<5.4", "symfony/proxy-manager-bridge": "<5.4", "symfony/yaml": "<5.4" diff --git a/src/Symfony/Component/HttpKernel/Bundle/AbstractBundle.php b/src/Symfony/Component/HttpKernel/Bundle/AbstractBundle.php new file mode 100644 index 0000000000000..3e6029f8c2510 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Bundle/AbstractBundle.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Bundle; + +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ConfigurableExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +/** + * A Bundle that provides configuration hooks. + * + * @author Yonel Ceruto + */ +abstract class AbstractBundle extends Bundle implements ConfigurableExtensionInterface +{ + protected string $extensionAlias = ''; + + public function configure(DefinitionConfigurator $definition): void + { + } + + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + } + + public function getContainerExtension(): ?ExtensionInterface + { + if ('' === $this->extensionAlias) { + $this->extensionAlias = Container::underscore(preg_replace('/Bundle$/', '', $this->getName())); + } + + return $this->extension ??= new BundleExtension($this, $this->extensionAlias); + } +} diff --git a/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.php b/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.php new file mode 100644 index 0000000000000..b80bc21f25cf7 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.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\Component\HttpKernel\Bundle; + +use Symfony\Component\Config\Definition\Configuration; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ConfigurableExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Extension\ExtensionTrait; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +/** + * @author Yonel Ceruto + * + * @internal + */ +class BundleExtension extends Extension implements PrependExtensionInterface +{ + use ExtensionTrait; + + public function __construct( + private ConfigurableExtensionInterface $subject, + private string $alias, + ) { + } + + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return new Configuration($this->subject, $container, $this->getAlias()); + } + + public function getAlias(): string + { + return $this->alias; + } + + public function prepend(ContainerBuilder $container): void + { + $callback = function (ContainerConfigurator $configurator) use ($container) { + $this->subject->prependExtension($configurator, $container); + }; + + $this->executeConfiguratorCallback($container, $callback, $this->subject); + } + + public function load(array $configs, ContainerBuilder $container): void + { + $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); + + $callback = function (ContainerConfigurator $configurator) use ($config, $container) { + $this->subject->loadExtension($config, $configurator, $container); + }; + + $this->executeConfiguratorCallback($container, $callback, $this->subject); + } +} diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 659d8e5902654..6dbcfca954884 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Deprecate StreamedResponseListener, it's not needed anymore * Add `Profiler::isEnabled()` so collaborating collector services may elect to omit themselves. * Add the `UidValueResolver` argument value resolver + * Add `AbstractBundle` class for DI configuration/definition on a single file 6.0 --- diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php index 7af756e0b8c10..ec12d626a2e26 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php @@ -13,8 +13,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; +use Symfony\Component\HttpKernel\Tests\Fixtures\AcmeFooBundle\AcmeFooBundle; class MergeExtensionConfigurationPassTest extends TestCase { @@ -31,6 +33,27 @@ public function testAutoloadMainExtension() $this->assertTrue($container->hasDefinition('loaded.foo')); $this->assertTrue($container->hasDefinition('not_loaded.bar')); } + + public function testFooBundle() + { + $bundle = new AcmeFooBundle(); + + $container = new ContainerBuilder(new ParameterBag([ + 'kernel.environment' => 'test', + 'kernel.build_dir' => sys_get_temp_dir(), + ])); + $container->registerExtension(new LoadedExtension()); + $container->registerExtension($bundle->getContainerExtension()); + + $configPass = new MergeExtensionConfigurationPass(['loaded', 'acme_foo']); + $configPass->process($container); + + $this->assertSame([[], ['bar' => 'baz']], $container->getExtensionConfig('loaded'), '->prependExtension() prepends an extension config'); + $this->assertTrue($container->hasDefinition('acme_foo.foo'), '->loadExtension() registers a service'); + $this->assertTrue($container->hasDefinition('acme_foo.bar'), '->loadExtension() imports a service'); + $this->assertTrue($container->hasParameter('acme_foo.config'), '->loadExtension() sets a parameter'); + $this->assertSame(['foo' => 'bar', 'ping' => 'pong'], $container->getParameter('acme_foo.config'), '->loadConfiguration() defines and loads configurations'); + } } class LoadedExtension extends Extension diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php new file mode 100644 index 0000000000000..4fba6260f9337 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures\AcmeFooBundle; + +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; + +class AcmeFooBundle extends AbstractBundle +{ + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->scalarNode('foo')->defaultValue('bar')->end() + ->end() + ; + + $definition->import('Resources/config/definition.php'); + } + + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->extension('loaded', ['bar' => 'baz']); + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->parameters() + ->set('acme_foo.config', $config) + ; + + $container->services() + ->set('acme_foo.foo', \stdClass::class) + ; + + $container->import('Resources/config/services.php'); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/Resources/config/definition.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/Resources/config/definition.php new file mode 100644 index 0000000000000..f57b1d79b8c4f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/Resources/config/definition.php @@ -0,0 +1,11 @@ +rootNode() + ->children() + ->scalarNode('ping')->defaultValue('pong')->end() + ->end() + ; +}; diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/Resources/config/services.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/Resources/config/services.php new file mode 100644 index 0000000000000..4bb62c7eb863e --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/AcmeFooBundle/Resources/config/services.php @@ -0,0 +1,9 @@ +services() + ->set('acme_foo.bar', \stdClass::class) + ; +}; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index da4b992f4555e..f8e2ceb160820 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -25,7 +25,7 @@ }, "require-dev": { "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.4|^6.0", + "symfony/config": "^6.1", "symfony/console": "^5.4|^6.0", "symfony/css-selector": "^5.4|^6.0", "symfony/dependency-injection": "^6.1", @@ -48,10 +48,10 @@ "conflict": { "symfony/browser-kit": "<5.4", "symfony/cache": "<5.4", - "symfony/config": "<5.4", + "symfony/config": "<6.1", "symfony/console": "<5.4", "symfony/form": "<5.4", - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.1", "symfony/doctrine-bridge": "<5.4", "symfony/http-client": "<5.4", "symfony/mailer": "<5.4",