From eaf9461722bc6e8e4179258ffa0e9ca905718e83 Mon Sep 17 00:00:00 2001 From: Ion Bazan Date: Wed, 20 Oct 2021 11:45:04 +0800 Subject: [PATCH] add suggestions for debug:firewall, debug:form, debug:messenger, debug:router --- .../Command/RouterDebugCommand.php | 16 ++++ .../Functional/RouterDebugCommandTest.php | 32 +++++++ .../Command/DebugFirewallCommand.php | 9 ++ .../Component/Form/Command/DebugCommand.php | 73 ++++++++++++--- .../Form/Tests/Command/DebugCommandTest.php | 93 +++++++++++++++++++ .../Messenger/Command/DebugCommand.php | 9 ++ .../Tests/Command/DebugCommandTest.php | 26 ++++++ 7 files changed, 245 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index 1ae5835447e1d..454d59cb3832c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -13,6 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -131,4 +133,18 @@ private function findRouteNameContaining(string $name, RouteCollection $routes): return $foundRoutesNames; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues(array_keys($this->router->getRouteCollection()->all())); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php index b7cf74798a232..e9a8cd143b802 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; /** @@ -69,6 +70,37 @@ public function testSearchWithThrow() $tester->execute(['name' => 'gerard'], ['interactive' => true]); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $tester = new CommandCompletionTester($this->application->get('debug:router')); + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions() + { + yield 'option --format' => [ + ['--format', ''], + ['txt', 'xml', 'json', 'md'], + ]; + + yield 'route_name' => [ + [''], + [ + 'routerdebug_session_welcome', + 'routerdebug_session_welcome_name', + 'routerdebug_session_logout', + 'routerdebug_test', + ], + ]; + } + private function createCommandTester(): CommandTester { $command = $this->application->get('debug:router'); diff --git a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php index 6aa705c55afe6..b5fe209944ce9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/DebugFirewallCommand.php @@ -15,6 +15,8 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallContext; use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -277,4 +279,11 @@ private function getExampleName(): string return $name; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('name')) { + $suggestions->suggestValues($this->firewallNames); + } + } } diff --git a/src/Symfony/Component/Form/Command/DebugCommand.php b/src/Symfony/Component/Form/Command/DebugCommand.php index 0d1b626e9f88b..a9e3c7661f65f 100644 --- a/src/Symfony/Component/Form/Command/DebugCommand.php +++ b/src/Symfony/Component/Form/Command/DebugCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Form\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -159,19 +161,7 @@ protected function execute(InputInterface $input, OutputInterface $output) private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, string $shortClassName): string { - $classes = []; - sort($this->namespaces); - foreach ($this->namespaces as $namespace) { - if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) { - $classes[] = $fqcn; - } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName))) { - $classes[] = $fqcn; - } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName).'Type')) { - $classes[] = $fqcn; - } elseif (str_ends_with($shortClassName, 'type') && class_exists($fqcn = $namespace.'\\'.ucfirst(substr($shortClassName, 0, -4).'Type'))) { - $classes[] = $fqcn; - } - } + $classes = $this->getFqcnTypeClasses($shortClassName); if (0 === $count = \count($classes)) { $message = sprintf("Could not find type \"%s\" into the following namespaces:\n %s", $shortClassName, implode("\n ", $this->namespaces)); @@ -198,6 +188,25 @@ private function getFqcnTypeClass(InputInterface $input, SymfonyStyle $io, strin return $io->choice(sprintf("The type \"%s\" is ambiguous.\n\nSelect one of the following form types to display its information:", $shortClassName), $classes, $classes[0]); } + private function getFqcnTypeClasses(string $shortClassName): array + { + $classes = []; + sort($this->namespaces); + foreach ($this->namespaces as $namespace) { + if (class_exists($fqcn = $namespace.'\\'.$shortClassName)) { + $classes[] = $fqcn; + } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName))) { + $classes[] = $fqcn; + } elseif (class_exists($fqcn = $namespace.'\\'.ucfirst($shortClassName).'Type')) { + $classes[] = $fqcn; + } elseif (str_ends_with($shortClassName, 'type') && class_exists($fqcn = $namespace.'\\'.ucfirst(substr($shortClassName, 0, -4).'Type'))) { + $classes[] = $fqcn; + } + } + + return $classes; + } + private function getCoreTypes(): array { $coreExtension = new CoreExtension(); @@ -242,4 +251,42 @@ private function findAlternatives(string $name, array $collection): array return array_keys($alternatives); } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('class')) { + $suggestions->suggestValues(array_merge($this->getCoreTypes(), $this->types)); + + return; + } + + if ($input->mustSuggestArgumentValuesFor('option') && null !== $class = $input->getArgument('class')) { + $this->completeOptions($class, $suggestions); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $helper = new DescriptorHelper(); + $suggestions->suggestValues($helper->getFormats()); + } + } + + private function completeOptions(string $class, CompletionSuggestions $suggestions): void + { + if (!class_exists($class) || !is_subclass_of($class, FormTypeInterface::class)) { + $classes = $this->getFqcnTypeClasses($class); + + if (1 === count($classes)) { + $class = $classes[0]; + } + } + + if (!$this->formRegistry->hasType($class)) { + return; + } + + $resolvedType = $this->formRegistry->getType($class); + $suggestions->suggestValues($resolvedType->getOptionsResolver()->getDefinedOptions()); + } } diff --git a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php index 6a22c4238db63..20067177f80aa 100644 --- a/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Form/Tests/Command/DebugCommandTest.php @@ -14,11 +14,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Command\DebugCommand; +use Symfony\Component\Form\Extension\Core\CoreExtension; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormRegistry; +use Symfony\Component\Form\FormTypeInterface; use Symfony\Component\Form\ResolvedFormTypeFactory; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -186,6 +189,96 @@ class:%s , $tester->getDisplay(true)); } + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $formRegistry = new FormRegistry([], new ResolvedFormTypeFactory()); + $command = new DebugCommand($formRegistry); + $application = new Application(); + $application->add($command); + $tester = new CommandCompletionTester($application->get('debug:form')); + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'option --format' => [ + ['--format', ''], + ['txt', 'json'], + ]; + + yield 'form_type' => [ + [''], + $this->getCoreTypes(), + ]; + + yield 'option for FQCN' => [ + ['Symfony\\Component\\Form\\Extension\\Core\\Type\\ButtonType', ''], + [ + 'block_name', + 'block_prefix', + 'disabled', + 'label', + 'label_format', + 'row_attr', + 'label_html', + 'label_translation_parameters', + 'attr_translation_parameters', + 'attr', + 'translation_domain', + 'auto_initialize', + 'priority', + ], + ]; + + yield 'option for short name' => [ + ['ButtonType', ''], + [ + 'block_name', + 'block_prefix', + 'disabled', + 'label', + 'label_format', + 'row_attr', + 'label_html', + 'label_translation_parameters', + 'attr_translation_parameters', + 'attr', + 'translation_domain', + 'auto_initialize', + 'priority', + ], + ]; + + yield 'option for ambiguous form type' => [ + ['Type', ''], + [], + ]; + + yield 'option for invalid form type' => [ + ['NotExistingFormType', ''], + [], + ]; + } + + private function getCoreTypes(): array + { + $coreExtension = new CoreExtension(); + $loadTypesRefMethod = (new \ReflectionObject($coreExtension))->getMethod('loadTypes'); + $loadTypesRefMethod->setAccessible(true); + $coreTypes = $loadTypesRefMethod->invoke($coreExtension); + $coreTypes = array_map(function (FormTypeInterface $type) { return \get_class($type); }, $coreTypes); + sort($coreTypes); + + return $coreTypes; + } + private function createCommandTester(array $namespaces = ['Symfony\Component\Form\Extension\Core\Type'], array $types = []) { $formRegistry = new FormRegistry([], new ResolvedFormTypeFactory()); diff --git a/src/Symfony/Component/Messenger/Command/DebugCommand.php b/src/Symfony/Component/Messenger/Command/DebugCommand.php index 90877eca744fa..4320ad731ad30 100644 --- a/src/Symfony/Component/Messenger/Command/DebugCommand.php +++ b/src/Symfony/Component/Messenger/Command/DebugCommand.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Messenger\Command; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -138,4 +140,11 @@ private static function getClassDescription(string $class): string return ''; } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('bus')) { + $suggestions->suggestValues(array_keys($this->mapping)); + } + } } diff --git a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php index 6127237da74a0..af2d0e3e9fa78 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Messenger\Tests\Command; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Messenger\Command\DebugCommand; use Symfony\Component\Messenger\Tests\Fixtures\DummyCommand; @@ -166,4 +168,28 @@ public function testExceptionOnUnknownBusArgument() $tester = new CommandTester($command); $tester->execute(['bus' => 'unknown_bus'], ['decorated' => false]); } + + /** + * @dataProvider provideCompletionSuggestions + */ + public function testComplete(array $input, array $expectedSuggestions) + { + if (!class_exists(CommandCompletionTester::class)) { + $this->markTestSkipped('Test command completion requires symfony/console 5.4+.'); + } + + $command = new DebugCommand(['command_bus' => [], 'query_bus' => []]); + $application = new Application(); + $application->add($command); + $tester = new CommandCompletionTester($application->get('debug:messenger')); + $this->assertSame($expectedSuggestions, $tester->complete($input)); + } + + public function provideCompletionSuggestions(): iterable + { + yield 'bus' => [ + [''], + ['command_bus', 'query_bus'], + ]; + } }