diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 2da2363dab205..1651cfab03182 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -91,6 +91,7 @@ CHANGELOG * Added a `InMemoryTransport` to Messenger. Use it with a DSN starting with `in-memory://`. * Added `framework.property_access.throw_exception_on_invalid_property_path` config option. * Added `cache:pool:list` command to list all available cache pools. + * Added `debug:autoconfiguration` command to display the autoconfiguration of interfaces/classes 4.2.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutoconfigurationCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutoconfigurationCommand.php new file mode 100644 index 0000000000000..b6b9d2770b858 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutoconfigurationCommand.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Dumper\YamlDumper; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\AbstractDumper; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +/** + * A console command for autoconfiguration information. + * + * @internal + */ +final class DebugAutoconfigurationCommand extends ContainerDebugCommand +{ + protected static $defaultName = 'debug:autoconfiguration'; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition([ + new InputArgument('search', InputArgument::OPTIONAL, 'A search filter'), + new InputOption('tags', null, InputOption::VALUE_NONE, 'Displays autoconfiguration interfaces/class grouped by tags'), + ]) + ->setDescription('Displays current autoconfiguration for an application') + ->setHelp(<<<'EOF' +The %command.name% command displays all services that are autoconfigured: + + php %command.full_name% + +You can also pass a search term to filter the list: + + php %command.full_name% log + +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $errorIo = $io->getErrorStyle(); + + $definitions = $this->getContainerBuilder()->getAutoconfiguredInstanceof(); + ksort($definitions, SORT_NATURAL); + + if ($search = $input->getArgument('search')) { + $definitions = array_filter($definitions, function ($key) use ($search) { + return false !== stripos(str_replace('\\', '', $key), $search); + }, ARRAY_FILTER_USE_KEY); + + if (0 === \count($definitions)) { + $errorIo->error(sprintf('No autoconfiguration interface/class found matching "%s"', $search)); + + return 1; + } + + $name = $this->findProperInterfaceName(array_keys($definitions), $input, $io, $search); + /** @var ChildDefinition $definition */ + $definition = $definitions[$name]; + + $io->title(sprintf('Information for Interface/Class "%s"', $name)); + $tableHeaders = ['Option', 'Value']; + $tableRows = []; + + $tagInformation = []; + foreach ($definition->getTags() as $tagName => $tagData) { + foreach ($tagData as $tagParameters) { + $parameters = array_map(function ($key, $value) { + return sprintf('%s: %s', $key, $value); + }, array_keys($tagParameters), array_values($tagParameters)); + $parameters = implode(', ', $parameters); + + if ('' === $parameters) { + $tagInformation[] = sprintf('%s', $tagName); + } else { + $tagInformation[] = sprintf('%s (%s)', $tagName, $parameters); + } + } + } + $tableRows[] = ['Tags', implode("\n", $tagInformation)]; + + $calls = $definition->getMethodCalls(); + if (\count($calls) > 0) { + $callInformation = []; + foreach ($calls as $call) { + $callInformation[] = $call[0]; + } + $tableRows[] = ['Calls', implode(', ', $callInformation)]; + } + + $io->table($tableHeaders, $tableRows); + } else { + $io->table(['Interface/Class'], array_map(static function ($interface) { + return [$interface]; + }, array_keys($definitions))); + } + + $io->newLine(); + + return 0; + } + + private function findProperInterfaceName(array $list, InputInterface $input, SymfonyStyle $io, string $name): string + { + $name = ltrim($name, '\\'); + + if (\in_array($name, $list, true)) { + return $name; + } + + if (1 === \count($list)) { + return $list[0]; + } + + return $io->choice('Select one of the following interfaces to display its information', $list); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 09d141b2d234f..0433f00d8148f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -428,7 +428,9 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(LocaleAwareInterface::class) ->addTag('kernel.locale_aware'); $container->registerForAutoconfiguration(ResetInterface::class) - ->addTag('kernel.reset', ['method' => 'reset']); + ->addTag('kernel.reset', ['method' => 'reset']) + ->addTag('kernel.reset2', ['method' => 'reset2']) + ; if (!interface_exists(MarshallerInterface::class)) { $container->registerForAutoconfiguration(ResettableInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index 6333f2d3cd0df..9f768f1b879b9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -74,6 +74,10 @@ + + + + null diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DebugAutoconfigurationBundle/Autoconfiguration/Bindings.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DebugAutoconfigurationBundle/Autoconfiguration/Bindings.php new file mode 100644 index 0000000000000..903fad94f3309 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DebugAutoconfigurationBundle/Autoconfiguration/Bindings.php @@ -0,0 +1,15 @@ +paramOne = $paramOne; + $this->paramTwo = $paramTwo; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DebugAutoconfigurationBundle/Autoconfiguration/MethodCalls.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DebugAutoconfigurationBundle/Autoconfiguration/MethodCalls.php new file mode 100644 index 0000000000000..52c78d1d77be8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DebugAutoconfigurationBundle/Autoconfiguration/MethodCalls.php @@ -0,0 +1,14 @@ +registerForAutoconfiguration(MethodCalls::class) + ->addMethodCall('setMethodOne', [new Reference('logger')]) + ->addMethodCall('setMethodTwo', [['paramOne', 'paramOne']]); + + $container->registerForAutoconfiguration(Bindings::class) + ->setBindings([ + '$paramOne' => new Reference('logger'), + '$paramTwo' => 'binding test', + ]); + + $container->registerForAutoconfiguration(TagsAttributes::class) + ->addTag('debugautoconfiguration.tag1', ['method' => 'debug']) + ->addTag('debugautoconfiguration.tag2', ['test']) + ; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutoconfigurationCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutoconfigurationCommandTest.php new file mode 100644 index 0000000000000..1ca56095265e5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutoconfigurationCommandTest.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\ApplicationTester; + +/** + * @group functional + */ +class DebugAutoconfigurationCommandTest extends AbstractWebTestCase +{ + public function testBasicFunctionality() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration']); + + $expectedOutput = <<assertStringContainsString($expectedOutput, $tester->getDisplay(true)); + } + + public function testSearchArgument() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration', 'search' => 'logger']); + + $this->assertStringContainsString('Psr\Log\LoggerAwareInterface', $tester->getDisplay(true)); + $this->assertStringNotContainsString('Sensio\Bundle\FrameworkExtraBundle', $tester->getDisplay(true)); + } + + public function testAutoconfigurationWithMethodCalls() + { + static::bootKernel(['test_case' => 'DebugAutoconfiguration', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration', 'search' => 'MethodCalls']); + + $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\Autoconfiguration\MethodCalls', $tester->getDisplay(true)); + $expectedMethodCallOutput = <<assertStringContainsString($expectedMethodCallOutput, $tester->getDisplay(true)); + } + + public function testAutoconfigurationWithMultipleTagsAttributes() + { + static::bootKernel(['test_case' => 'DebugAutoconfiguration', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration', 'search' => 'TagsAttributes']); + + $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\Autoconfiguration\TagsAttributes', $tester->getDisplay(true)); + $expectedTagsAttributesOutput = << "debug" + ] + + Tag debugautoconfiguration.tag2 + Tag attribute [ + "test" + ] +EOD; + $this->assertStringContainsString($expectedTagsAttributesOutput, $tester->getDisplay(true)); + } + + public function testAutoconfigurationWithBindings() + { + static::bootKernel(['test_case' => 'DebugAutoconfiguration', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration', 'search' => 'Bindings']); + + $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\Autoconfiguration\Bindings', $tester->getDisplay(true)); + $expectedTagsAttributesOutput = <<<'EOD' + Bindings $paramOne: '@logger' + $paramTwo: 'binding test' +EOD; + $this->assertStringContainsString($expectedTagsAttributesOutput, $tester->getDisplay(true)); + } + + public function testSearchIgnoreBackslashWhenFindingInterfaceOrClass() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration', 'search' => 'PsrLogLoggerAwareInterface']); + $this->assertStringContainsString('Psr\Log\LoggerAwareInterface', $tester->getDisplay(true)); + } + + public function testSearchNoResults() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autoconfiguration', 'search' => 'foo_fake'], ['capture_stderr_separately' => true]); + + $this->assertStringContainsString('No autoconfiguration interface/class found matching "foo_fake"', $tester->getErrorOutput()); + $this->assertSame(1, $tester->getStatusCode()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/DebugAutoconfiguration/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/DebugAutoconfiguration/bundles.php new file mode 100644 index 0000000000000..9d2801b13df9d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/DebugAutoconfiguration/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\DebugAutoconfigurationBundle; + +return [ + new FrameworkBundle(), + new DebugAutoconfigurationBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/DebugAutoconfiguration/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/DebugAutoconfiguration/config.yml new file mode 100644 index 0000000000000..f76eb28f92587 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/DebugAutoconfiguration/config.yml @@ -0,0 +1,3 @@ +imports: +- { resource: ../config/default.yml } + diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index 6f7d918d26af4..768832d08a296 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -53,6 +54,7 @@ public function dump(array $options = []) $this->addParameters($container); $this->addServices($container); + $this->addAutoconfiguredInstanceof($container); $this->document->appendChild($container); $xml = $this->document->saveXML(); @@ -337,6 +339,47 @@ private function convertParameters(array $parameters, string $type, \DOMElement } } + private function addAutoconfiguredInstanceof(\DOMElement $parent) + { + $childDefinitions = $this->container->getAutoconfiguredInstanceof(); + + if (!$childDefinitions) { + return; + } + + $autoconfiguredInstanceOf = $this->document->createElement('autoconfigured-instanceof'); + + foreach ($childDefinitions as $id => $definition) { + $this->addAutoconfiguredInstanceofItem($definition, $id, $autoconfiguredInstanceOf); + } + +// dump($this->container); +// die; + + $parent->appendChild($autoconfiguredInstanceOf); + } + + private function addAutoconfiguredInstanceofItem(ChildDefinition $definition, string $id, \DOMElement $parent) + { + $item = $this->document->createElement('autoconfigured-instanceof-item'); + $item->setAttribute('id', $id); + + foreach ($definition->getTags() as $name => $tags) { + foreach ($tags as $attributes) { + $tag = $this->document->createElement('tag'); + $tag->setAttribute('name', $name); + foreach ($attributes as $key => $value) { + $tag->setAttribute($key, $value); + } + $item->appendChild($tag); + } + } + + $this->addMethodCalls($definition->getMethodCalls(), $item); + + $parent->appendChild($item); + } + /** * Escapes arguments. */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 6cad9453048e7..7e10b500a3ad4 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -71,6 +71,9 @@ public function load($resource, string $type = null) $this->instanceof = []; $this->registerAliasesForSinglyImplementedInterfaces(); } + + // autoconfiguredInstanceof + $this->parseAutoconfiguredInstanceOf($xml, $path); } /** @@ -112,6 +115,51 @@ private function parseImports(\DOMDocument $xml, string $file) } } + private function parseAutoconfiguredInstanceOf(\DOMDocument $xml, string $file) + { + $xpath = new \DOMXPath($xml); + $xpath->registerNamespace('container', self::NS); + + if (false === $autoconfiguredInstanceof = $xpath->query(('//container:autoconfigured-instanceof/container:autoconfigured-instanceof-item'))) { + return; + } + + foreach ($autoconfiguredInstanceof as $item) { + $definition = $this->container->registerForAutoconfiguration($item->getAttribute('id')); + + foreach ($this->getChildren($item, 'call') as $call) { + $definition->addMethodCall($call->getAttribute('method'), $this->getArgumentsAsPhp($call, 'argument', $file), XmlUtils::phpize($call->getAttribute('returns-clone'))); + } + + $tags = $this->getChildren($item, 'tag'); + + if (!empty($defaults['tags'])) { + $tags = array_merge($tags, $defaults['tags']); + } + + foreach ($tags as $tag) { + $parameters = []; + foreach ($tag->attributes as $name => $node) { + if ('name' === $name) { + continue; + } + + if (false !== strpos($name, '-') && false === strpos($name, '_') && !\array_key_exists($normalizedName = str_replace('-', '_', $name), $parameters)) { + $parameters[$normalizedName] = XmlUtils::phpize($node->nodeValue); + } + // keep not normalized key + $parameters[$name] = XmlUtils::phpize($node->nodeValue); + } + + if ('' === $tag->getAttribute('name')) { + throw new InvalidArgumentException(sprintf('The tag name for service "%s" in %s must be a non-empty string.', (string) $item->getAttribute('id'), $file)); + } + + $definition->addTag($tag->getAttribute('name'), $parameters); + } + } + } + private function parseDefinitions(\DOMDocument $xml, string $file, array $defaults) { $xpath = new \DOMXPath($xml); diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index d2c81bcf311c7..c504e36acabe0 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -37,6 +37,10 @@ + + + + @@ -60,6 +64,25 @@ + + + + + + + + + + + + + + + + +