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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+