diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php index 4dbdc4c7abb81..a72034d98293a 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Configuration.php @@ -26,7 +26,9 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder = new TreeBuilder('debug'); $rootNode = $treeBuilder->getRootNode(); - $rootNode->children() + $rootNode + ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/debug.html', 'symfony/debug-bundle') + ->children() ->integerNode('max_items') ->info('Max number of displayed items past the first level, -1 means no limit.') ->min(-1) diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index d00a4db6424c0..7756b7fd73014 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -18,13 +18,14 @@ "require": { "php": ">=8.2", "ext-xml": "*", + "composer-runtime-api": ">=2.1", "symfony/dependency-injection": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/twig-bridge": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", + "symfony/config": "^7.3", "symfony/web-profiler-bundle": "^6.4|^7.0" }, "conflict": { diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php index 55c101e9c29e3..8d5f85ceea4ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php @@ -104,6 +104,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->title( \sprintf('Current configuration for %s', $name === $extensionAlias ? \sprintf('extension with alias "%s"', $extensionAlias) : \sprintf('"%s"', $name)) ); + + if ($docUrl = $this->getDocUrl($extension, $container)) { + $io->comment(\sprintf('Documentation at %s', $docUrl)); + } } $io->writeln($this->convertToFormat([$extensionAlias => $config], $format)); @@ -269,4 +273,15 @@ private function getAvailableFormatOptions(): array { return ['txt', 'yaml', 'json']; } + + private function getDocUrl(ExtensionInterface $extension, ContainerBuilder $container): ?string + { + $configuration = $extension instanceof ConfigurationInterface ? $extension : $extension->getConfiguration($container->getExtensionConfig($extension->getAlias()), $container); + + return $configuration + ->getConfigTreeBuilder() + ->getRootNode() + ->getNode(true) + ->getAttribute('docUrl'); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index 7e5cd765fd2d3..3cb744d746cae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -23,6 +23,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; use Symfony\Component\Yaml\Yaml; /** @@ -123,6 +124,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $message .= \sprintf(' at path "%s"', $path); } + if ($docUrl = $this->getExtensionDocUrl($extension)) { + $message .= \sprintf(' (see %s)', $docUrl); + } + switch ($format) { case 'yaml': $io->writeln(\sprintf('# %s', $message)); @@ -182,4 +187,18 @@ private function getAvailableFormatOptions(): array { return ['yaml', 'xml']; } + + private function getExtensionDocUrl(ConfigurationInterface|ConfigurationExtensionInterface $extension): ?string + { + $kernel = $this->getApplication()->getKernel(); + $container = $this->getContainerBuilder($kernel); + + $configuration = $extension instanceof ConfigurationInterface ? $extension : $extension->getConfiguration($container->getExtensionConfig($extension->getAlias()), $container); + + return $configuration + ->getConfigTreeBuilder() + ->getRootNode() + ->getNode(true) + ->getAttribute('docUrl'); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index aa61cb12c56f4..0f882d3563ebd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -75,6 +75,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode = $treeBuilder->getRootNode(); $rootNode + ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/framework.html', 'symfony/framework-bundle') ->beforeNormalization() ->ifTrue(fn ($v) => !isset($v['assets']) && isset($v['templating']) && class_exists(Package::class)) ->then(function ($v) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 9854a1f047a7a..9b7414de5e532 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -55,6 +55,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode = $tb->getRootNode(); $rootNode + ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/security.html', 'symfony/security-bundle') ->beforeNormalization() ->always() ->then(function ($v) { diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index fa5cb52ff04b5..7459b0175b95f 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -20,7 +20,7 @@ "composer-runtime-api": ">=2.1", "ext-xml": "*", "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", + "symfony/config": "^7.3", "symfony/dependency-injection": "^6.4.11|^7.1.4", "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index 32a4bb318fea4..0c56f8e328c3f 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -32,7 +32,9 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder = new TreeBuilder('twig'); $rootNode = $treeBuilder->getRootNode(); - $rootNode->beforeNormalization() + $rootNode + ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/twig.html', 'symfony/twig-bundle') + ->beforeNormalization() ->ifTrue(fn ($v) => \is_array($v) && \array_key_exists('exception_controller', $v)) ->then(function ($v) { if (isset($v['exception_controller'])) { diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index f6e0e110cc686..be9ef84a61cf3 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=8.2", "composer-runtime-api": ">=2.1", - "symfony/config": "^6.4|^7.0", + "symfony/config": "^7.3", "symfony/dependency-injection": "^6.4|^7.0", "symfony/twig-bridge": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php index d9ca50a27af21..649bf459e8fed 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/Configuration.php @@ -31,7 +31,9 @@ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('web_profiler'); - $treeBuilder->getRootNode() + $treeBuilder + ->getRootNode() + ->docUrl('https://symfony.com/doc/{version:major}.{version:minor}/reference/configuration/web_profiler.html', 'symfony/web-profiler-bundle') ->children() ->arrayNode('toolbar') ->info('Profiler toolbar configuration') diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index ce94b4b62ebbb..c0f8149295c19 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -17,7 +17,8 @@ ], "require": { "php": ">=8.2", - "symfony/config": "^6.4|^7.0", + "composer-runtime-api": ">=2.1", + "symfony/config": "^7.3", "symfony/framework-bundle": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/routing": "^6.4|^7.0", diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 0a9a6c0e08372..6ee63f82c72ff 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `ExprBuilder::ifFalse()` * Add support for info on `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()` * Allow using an enum FQCN with `EnumNode` + * Add `NodeDefinition::docUrl()` 7.2 --- diff --git a/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php b/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php index 54e976e246ec6..fdfbdabd29ad0 100644 --- a/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php +++ b/src/Symfony/Component/Config/Definition/Builder/NodeDefinition.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Config\Definition\Builder; +use Composer\InstalledVersions; use Symfony\Component\Config\Definition\BaseNode; use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; use Symfony\Component\Config\Definition\NodeInterface; @@ -76,6 +77,26 @@ public function example(string|array $example): static return $this->attribute('example', $example); } + /** + * Sets the documentation URI, as usually put in the "@see" tag of a doc block. This + * can either be a URL or a file path. You can use the placeholders {package}, + * {version:major} and {version:minor} in the URI. + * + * @return $this + */ + public function docUrl(string $uri, ?string $package = null): static + { + if ($package) { + preg_match('/^(\d+)\.(\d+)\.(\d+)/', InstalledVersions::getVersion($package) ?? '', $m); + } + + return $this->attribute('docUrl', strtr($uri, [ + '{package}' => $package ?? '', + '{version:major}' => $m[1] ?? '', + '{version:minor}' => $m[2] ?? '', + ])); + } + /** * Sets an attribute on the node. * diff --git a/src/Symfony/Component/Config/Tests/Definition/Builder/NodeDefinitionTest.php b/src/Symfony/Component/Config/Tests/Definition/Builder/NodeDefinitionTest.php index 68c1ddff00d91..baa4518006bb6 100644 --- a/src/Symfony/Component/Config/Tests/Definition/Builder/NodeDefinitionTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/Builder/NodeDefinitionTest.php @@ -35,4 +35,35 @@ public function testSetPathSeparatorChangesChildren() $parentNode->setPathSeparator('/'); } + + public function testDocUrl() + { + $node = new ArrayNodeDefinition('node'); + $node->docUrl('https://example.com/doc/{package}/{version:major}.{version:minor}', 'phpunit/phpunit'); + + $r = new \ReflectionObject($node); + $p = $r->getProperty('attributes'); + + $this->assertMatchesRegularExpression('~^https://example.com/doc/phpunit/phpunit/\d+\.\d+$~', $p->getValue($node)['docUrl']); + } + + public function testDocUrlWithoutPackage() + { + $node = new ArrayNodeDefinition('node'); + $node->docUrl('https://example.com/doc/empty{version:major}.empty{version:minor}'); + + $r = new \ReflectionObject($node); + $p = $r->getProperty('attributes'); + + $this->assertSame('https://example.com/doc/empty.empty', $p->getValue($node)['docUrl']); + } + + public function testUnknownPackageThrowsException() + { + $this->expectException(\OutOfBoundsException::class); + $this->expectExceptionMessage('Package "phpunit/invalid" is not installed'); + + $node = new ArrayNodeDefinition('node'); + $node->docUrl('https://example.com/doc/{package}/{version:major}.{version:minor}', 'phpunit/invalid'); + } }