diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 75b89f95e5b6a..54c94ff4279d7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -23,6 +23,7 @@ CHANGELOG * Deprecated `CachePoolClearerPass`. Use `Symfony\Component\Cache\DependencyInjection\CachePoolClearerPass` instead. * Deprecated `CachePoolPass`. Use `Symfony\Component\Cache\DependencyInjection\CachePoolPass` instead. * Deprecated `CachePoolPrunerPass`. Use `Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass` instead. + * Added `debug:autoconfiguration` command to display the autoconfiguration of interfaces/classes 4.1.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..fb620cc255310 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutoconfigurationCommand.php @@ -0,0 +1,171 @@ + + * + * 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 + */ +class DebugAutoconfigurationCommand extends Command +{ + protected static $defaultName = 'debug:autoconfiguration'; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDefinition(array( + 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) + { + $io = new SymfonyStyle($input, $output); + $errorIo = $io->getErrorStyle(); + + $autoconfiguredInstanceofItems = $this->getContainerBuilder()->getAutoconfiguredInstanceof(); + + if ($search = $input->getArgument('search')) { + $autoconfiguredInstanceofItems = array_filter($autoconfiguredInstanceofItems, function ($key) use ($search) { + return false !== stripos(str_replace('\\', '', $key), $search); + }, ARRAY_FILTER_USE_KEY); + + if (empty($autoconfiguredInstanceofItems)) { + $errorIo->error(sprintf('No autoconfiguration interface/class found matching "%s"', $search)); + + return 1; + } + } + + ksort($autoconfiguredInstanceofItems, SORT_NATURAL); + + $io->title('Autoconfiguration'); + if ($search) { + $io->text(sprintf('(only showing classes/interfaces matching %s)', $search)); + } + $io->newLine(); + + /** @var ChildDefinition $autoconfiguredInstanceofItem */ + foreach ($autoconfiguredInstanceofItems as $key => $autoconfiguredInstanceofItem) { + $tableRows = array(); + + foreach ($autoconfiguredInstanceofItem->getTags() as $tag => $tagAttributes) { + $tableRows[] = array('Tag', $tag); + if ($tagAttributes !== array(array())) { + $tableRows[] = array('Tag attribute', $this->dumpTagAttribute($tagAttributes)); + } + } + + if ($autoconfiguredInstanceofItem->getMethodCalls()) { + $tableRows[] = array('Method call', $this->dumpMethodCall($autoconfiguredInstanceofItem)); + } + + if ($autoconfiguredInstanceofItem->getBindings()) { + $tableRows[] = array('Bindings', $this->dumpBindings($autoconfiguredInstanceofItem)); + } + + $io->writeln(sprintf("Autoconfiguration for \"%s\"\n==============================================", $key)); + $io->newLine(); + $io->table(array('Option', 'Value'), $tableRows); + } + } + + private function dumpMethodCall(ChildDefinition $autoconfiguredInstanceofItem) + { + $tagContainerBuilder = new ContainerBuilder(); + foreach ($tagContainerBuilder->getServiceIds() as $serviceId) { + $tagContainerBuilder->removeDefinition($serviceId); + $tagContainerBuilder->removeAlias($serviceId); + } + $tagContainerBuilder->addDefinitions(array($autoconfiguredInstanceofItem)); + + $dumper = new YamlDumper($tagContainerBuilder); + preg_match('/calls\:\n((?: +- .+\n)+)/', $dumper->dump(), $matches); + + return preg_replace('/^\s+/m', '', $matches[1]); + } + + private function dumpBindings(ChildDefinition $autoconfiguredInstanceofItem) + { + $tagContainerBuilder = new ContainerBuilder(); + foreach ($tagContainerBuilder->getServiceIds() as $serviceId) { + $tagContainerBuilder->removeDefinition($serviceId); + $tagContainerBuilder->removeAlias($serviceId); + } + + $dumper = new YamlDumper($tagContainerBuilder); + foreach ($autoconfiguredInstanceofItem->getBindings() as $bindingKey => $bindingValue) { + $tagContainerBuilder->setParameter($bindingKey, $bindingValue->getValues()[0]); + } + + preg_match('/parameters\:\n((?: + .+\n)+)/', $dumper->dump(), $matches); + + return preg_replace('/^\s+/m', '', $matches[1]); + } + + private function dumpTagAttribute(array $tagAttribute) + { + $cloner = new VarCloner(); + $cliDumper = new CliDumper(null, null, AbstractDumper::DUMP_LIGHT_ARRAY); + + return $cliDumper->dump($cloner->cloneVar(current($tagAttribute)), true); + } + + /** + * @return ContainerBuilder + */ + private function getContainerBuilder() + { + $kernel = $this->getApplication()->getKernel(); + $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); + $container = $buildContainer(); + $container->getCompilerPassConfig()->setRemovingPasses(array()); + $container->compile(); + + return $container; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 2e64bff2c03d7..7e55f305ff0c6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -336,7 +336,9 @@ public function load(array $configs, ContainerBuilder $container) $container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('kernel.event_subscriber'); $container->registerForAutoconfiguration(ResetInterface::class) - ->addTag('kernel.reset', array('method' => 'reset')); + ->addTag('kernel.reset', array('method' => 'reset')) + ->addTag('kernel.reset2', array('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 9553d4d60d7ce..8ec48cfa73506 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -60,6 +60,10 @@ + + + + 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', array(new Reference('logger'))) + ->addMethodCall('setMethodTwo', array(array('paramOne', 'paramOne'))); + + $container->registerForAutoconfiguration(Bindings::class) + ->setBindings(array( + '$paramOne' => new Reference('logger'), + '$paramTwo' => 'binding test', + )); + + $container->registerForAutoconfiguration(TagsAttributes::class) + ->addTag('debugautoconfiguration.tag1', array('method' => 'debug')) + ->addTag('debugautoconfiguration.tag2', array('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..504200effb4f3 --- /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 WebTestCase +{ + public function testBasicFunctionality() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration')); + + $expectedOutput = <<assertContains($expectedOutput, $tester->getDisplay(true)); + } + + public function testSearchArgument() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration', 'search' => 'logger')); + + $this->assertContains('Psr\Log\LoggerAwareInterface', $tester->getDisplay(true)); + $this->assertNotContains('Sensio\Bundle\FrameworkExtraBundle', $tester->getDisplay(true)); + } + + public function testAutoconfigurationWithMethodCalls() + { + static::bootKernel(array('test_case' => 'DebugAutoconfiguration', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration', 'search' => 'MethodCalls')); + + $this->assertContains('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\Autoconfiguration\MethodCalls', $tester->getDisplay(true)); + $expectedMethodCallOutput = <<assertContains($expectedMethodCallOutput, $tester->getDisplay(true)); + } + + public function testAutoconfigurationWithMultipleTagsAttributes() + { + static::bootKernel(array('test_case' => 'DebugAutoconfiguration', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration', 'search' => 'TagsAttributes')); + + $this->assertContains('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\Autoconfiguration\TagsAttributes', $tester->getDisplay(true)); + $expectedTagsAttributesOutput = << "debug" + ] + + Tag debugautoconfiguration.tag2 + Tag attribute [ + "test" + ] +EOD; + $this->assertContains($expectedTagsAttributesOutput, $tester->getDisplay(true)); + } + + public function testAutoconfigurationWithBindings() + { + static::bootKernel(array('test_case' => 'DebugAutoconfiguration', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration', 'search' => 'Bindings')); + + $this->assertContains('Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DebugAutoconfigurationBundle\Autoconfiguration\Bindings', $tester->getDisplay(true)); + $expectedTagsAttributesOutput = <<<'EOD' + Bindings $paramOne: '@logger' + $paramTwo: 'binding test' +EOD; + $this->assertContains($expectedTagsAttributesOutput, $tester->getDisplay(true)); + } + + public function testSearchIgnoreBackslashWhenFindingInterfaceOrClass() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration', 'search' => 'PsrLogLoggerAwareInterface')); + $this->assertContains('Psr\Log\LoggerAwareInterface', $tester->getDisplay(true)); + } + + public function testSearchNoResults() + { + static::bootKernel(array('test_case' => 'ContainerDebug', 'root_config' => 'config.yml')); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(array('command' => 'debug:autoconfiguration', 'search' => 'foo_fake'), array('capture_stderr_separately' => true)); + + $this->assertContains('No autoconfiguration interface/class found matching "foo_fake"', $tester->getErrorOutput()); + $this->assertEquals(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..3781e126e11de --- /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 array( + 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 } +