From 3191f587fdae820e3e27a0cc919a3b5c52f37ad2 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Thu, 29 May 2025 14:00:57 +0200 Subject: [PATCH 01/36] Replace get_class() calls by ::class --- Debug/CliRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Debug/CliRequest.php b/Debug/CliRequest.php index b023db07a..6e2c1012b 100644 --- a/Debug/CliRequest.php +++ b/Debug/CliRequest.php @@ -24,7 +24,7 @@ public function __construct( public readonly TraceableCommand $command, ) { parent::__construct( - attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'], + attributes: ['_controller' => $command->command::class, '_virtual_type' => 'command'], server: $_SERVER, ); } From aa64416f1d5a61edc084adea2a8528906858fcd2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jun 2025 16:08:14 +0200 Subject: [PATCH 02/36] Allow Symfony ^8.0 --- composer.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 65d69913a..109cdd762 100644 --- a/composer.json +++ b/composer.json @@ -20,19 +20,19 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", "psr/log": "^1|^2|^3" }, "provide": { From 5e7cd96684d8d229b9478fb0d224c60bf0cb2c3d Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sat, 7 Jun 2025 17:59:31 +0200 Subject: [PATCH 03/36] [Console] Fix setting aliases & hidden via name --- CHANGELOG.md | 5 +++++ Command/Command.php | 6 +++--- Tests/Command/CommandTest.php | 13 +++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3ae3d7d..509a9d03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.4 +--- + + * Allow setting aliases and the hidden flag via the command name passed to the constructor + 7.3 --- diff --git a/Command/Command.php b/Command/Command.php index f6cd84997..23e3b6621 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -97,13 +97,13 @@ public function __construct(?string $name = null) if (self::class !== (new \ReflectionMethod($this, 'getDefaultName'))->class) { trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class); - $defaultName = static::getDefaultName(); + $name = static::getDefaultName(); } else { - $defaultName = $attribute?->name; + $name = $attribute?->name; } } - if (null === $name && null !== $name = $defaultName) { + if (null !== $name) { $aliases = explode('|', $name); if ('' === $name = array_shift($aliases)) { diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index 0db3572fc..85442c7b9 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -205,6 +205,19 @@ public function testGetSetAliases() $this->assertEquals(['name1'], $command->getAliases(), '->setAliases() sets the aliases'); } + /** + * @testWith ["name|alias1|alias2", "name", ["alias1", "alias2"], false] + * ["|alias1|alias2", "alias1", ["alias2"], true] + */ + public function testSetAliasesAndHiddenViaName(string $name, string $expectedName, array $expectedAliases, bool $expectedHidden) + { + $command = new Command($name); + + self::assertSame($expectedName, $command->getName()); + self::assertSame($expectedHidden, $command->isHidden()); + self::assertSame($expectedAliases, $command->getAliases()); + } + public function testGetSynopsis() { $command = new \TestCommand(); From 32c3794a2617f993a2ea39ddc0ec5076b4a3923e Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 9 Jun 2025 17:40:54 +0200 Subject: [PATCH 04/36] [Console] Simplify using invokable commands when the component is used standalone --- Application.php | 39 ++- CHANGELOG.md | 2 + SingleCommandApplication.php | 2 +- Tests/ApplicationTest.php | 234 ++++++++++++------ Tests/Command/CommandTest.php | 4 +- Tests/Command/CompleteCommandTest.php | 2 +- Tests/Command/HelpCommandTest.php | 2 +- Tests/Command/ListCommandTest.php | 8 +- Tests/ConsoleEventsTest.php | 2 +- .../Descriptor/ApplicationDescriptionTest.php | 2 +- Tests/Fixtures/DescriptorApplication2.php | 8 +- .../DescriptorApplicationMbString.php | 2 +- Tests/Tester/CommandTesterTest.php | 2 +- Tests/phpt/alarm/command_exit.phpt | 2 +- Tests/phpt/signal/command_exit.phpt | 2 +- 15 files changed, 207 insertions(+), 106 deletions(-) diff --git a/Application.php b/Application.php index b4539fa1e..f77d57299 100644 --- a/Application.php +++ b/Application.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\CompleteCommand; use Symfony\Component\Console\Command\DumpCompletionCommand; @@ -28,6 +29,7 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\NamespaceNotFoundException; use Symfony\Component\Console\Exception\RuntimeException; @@ -512,7 +514,7 @@ public function getLongVersion(): string */ public function register(string $name): Command { - return $this->add(new Command($name)); + return $this->addCommand(new Command($name)); } /** @@ -520,25 +522,50 @@ public function register(string $name): Command * * If a Command is not enabled it will not be added. * - * @param Command[] $commands An array of commands + * @param callable[]|Command[] $commands An array of commands */ public function addCommands(array $commands): void { foreach ($commands as $command) { - $this->add($command); + $this->addCommand($command); } } + /** + * @deprecated since Symfony 7.4, use Application::addCommand() instead + */ + public function add(Command $command): ?Command + { + trigger_deprecation('symfony/console', '7.4', 'The "%s()" method is deprecated and will be removed in Symfony 8.0, use "%s::addCommand()" instead.', __METHOD__, self::class); + + return $this->addCommand($command); + } + /** * Adds a command object. * * If a command with the same name already exists, it will be overridden. * If the command is not enabled it will not be added. */ - public function add(Command $command): ?Command + public function addCommand(callable|Command $command): ?Command { $this->init(); + if (!$command instanceof Command) { + if (!\is_object($command) || $command instanceof \Closure) { + throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)); + } + + /** @var AsCommand $attribute */ + $attribute = ((new \ReflectionObject($command))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance() + ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class)); + + $command = (new Command($attribute->name)) + ->setDescription($attribute->description ?? '') + ->setHelp($attribute->help ?? '') + ->setCode($command); + } + $command->setApplication($this); if (!$command->isEnabled()) { @@ -604,7 +631,7 @@ public function has(string $name): bool { $this->init(); - return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name))); + return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->addCommand($this->commandLoader->get($name))); } /** @@ -1322,7 +1349,7 @@ private function init(): void $this->initialized = true; foreach ($this->getDefaultCommands() as $command) { - $this->add($command); + $this->addCommand($command); } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 509a9d03c..f481d55aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG --- * Allow setting aliases and the hidden flag via the command name passed to the constructor + * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone + * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` 7.3 --- diff --git a/SingleCommandApplication.php b/SingleCommandApplication.php index 2b54fb870..837948d12 100644 --- a/SingleCommandApplication.php +++ b/SingleCommandApplication.php @@ -57,7 +57,7 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu $application->setAutoExit($this->autoExit); // Fix the usage of the command displayed with "--help" $this->setName($_SERVER['argv'][0]); - $application->add($this); + $application->addCommand($this); $application->setDefaultCommand($this->getName(), true); $this->running = true; diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 268f8ba50..e5d16d7fe 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; +use Symfony\Component\Console\Command\InvokableCommand; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; @@ -28,6 +29,8 @@ use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\NamespaceNotFoundException; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\HelperSet; @@ -163,7 +166,7 @@ public function testAll() $commands = $application->all(); $this->assertInstanceOf(HelpCommand::class, $commands['help'], '->all() returns the registered commands'); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $commands = $application->all('foo'); $this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); } @@ -174,7 +177,7 @@ public function testAllWithCommandLoader() $commands = $application->all(); $this->assertInstanceOf(HelpCommand::class, $commands['help'], '->all() returns the registered commands'); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $commands = $application->all('foo'); $this->assertCount(1, $commands, '->all() takes a namespace as its first argument'); @@ -221,7 +224,7 @@ public function testRegisterAmbiguous() public function testAdd() { $application = new Application(); - $application->add($foo = new \FooCommand()); + $application->addCommand($foo = new \FooCommand()); $commands = $application->all(); $this->assertEquals($foo, $commands['foo:bar'], '->add() registers a command'); @@ -236,7 +239,60 @@ public function testAddCommandWithEmptyConstructor() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor.'); - (new Application())->add(new \Foo5Command()); + (new Application())->addCommand(new \Foo5Command()); + } + + public function testAddCommandWithExtendedCommand() + { + $application = new Application(); + $application->addCommand($foo = new \FooCommand()); + $commands = $application->all(); + + $this->assertEquals($foo, $commands['foo:bar']); + } + + public function testAddCommandWithInvokableCommand() + { + $application = new Application(); + $application->addCommand($foo = new InvokableTestCommand()); + $commands = $application->all(); + + $this->assertInstanceOf(Command::class, $command = $commands['invokable']); + $this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command)); + } + + public function testAddCommandWithInvokableExtendedCommand() + { + $application = new Application(); + $application->addCommand($foo = new InvokableExtendedTestCommand()); + $commands = $application->all(); + + $this->assertEquals($foo, $commands['invokable-extended']); + } + + /** + * @dataProvider provideInvalidInvokableCommands + */ + public function testAddCommandThrowsExceptionOnInvalidCommand(callable $command, string $expectedException, string $expectedExceptionMessage) + { + $application = new Application(); + + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + $application->addCommand($command); + } + + public static function provideInvalidInvokableCommands(): iterable + { + yield 'a function' => ['strlen', InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)]; + yield 'a closure' => [function () { + }, InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)]; + yield 'without the #[AsCommand] attribute' => [new class { + public function __invoke() + { + } + }, LogicException::class, \sprintf('The command must use the "%s" attribute.', AsCommand::class)]; } public function testHasGet() @@ -245,13 +301,13 @@ public function testHasGet() $this->assertTrue($application->has('list'), '->has() returns true if a named command is registered'); $this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered'); - $application->add($foo = new \FooCommand()); + $application->addCommand($foo = new \FooCommand()); $this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered'); $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name'); $this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias'); $application = new Application(); - $application->add($foo = new \FooCommand()); + $application->addCommand($foo = new \FooCommand()); // simulate --help $r = new \ReflectionObject($application); $p = $r->getProperty('wantHelps'); @@ -266,7 +322,7 @@ public function testHasGetWithCommandLoader() $this->assertTrue($application->has('list'), '->has() returns true if a named command is registered'); $this->assertFalse($application->has('afoobar'), '->has() returns false if a named command is not registered'); - $application->add($foo = new \FooCommand()); + $application->addCommand($foo = new \FooCommand()); $this->assertTrue($application->has('afoobar'), '->has() returns true if an alias is registered'); $this->assertEquals($foo, $application->get('foo:bar'), '->get() returns a command by name'); $this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias'); @@ -307,35 +363,35 @@ public function testGetInvalidCommand() public function testGetNamespaces() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); $this->assertEquals(['foo'], $application->getNamespaces(), '->getNamespaces() returns an array of unique used namespaces'); } public function testFindNamespace() { $application = new Application(); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns the given namespace if it exists'); $this->assertEquals('foo', $application->findNamespace('f'), '->findNamespace() finds a namespace given an abbreviation'); - $application->add(new \Foo2Command()); + $application->addCommand(new \Foo2Command()); $this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns the given namespace if it exists'); } public function testFindNamespaceWithSubnamespaces() { $application = new Application(); - $application->add(new \FooSubnamespaced1Command()); - $application->add(new \FooSubnamespaced2Command()); + $application->addCommand(new \FooSubnamespaced1Command()); + $application->addCommand(new \FooSubnamespaced2Command()); $this->assertEquals('foo', $application->findNamespace('foo'), '->findNamespace() returns commands even if the commands are only contained in subnamespaces'); } public function testFindAmbiguousNamespace() { $application = new Application(); - $application->add(new \BarBucCommand()); - $application->add(new \FooCommand()); - $application->add(new \Foo2Command()); + $application->addCommand(new \BarBucCommand()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo2Command()); $expectedMsg = "The namespace \"f\" is ambiguous.\nDid you mean one of these?\n foo\n foo1"; @@ -348,8 +404,8 @@ public function testFindAmbiguousNamespace() public function testFindNonAmbiguous() { $application = new Application(); - $application->add(new \TestAmbiguousCommandRegistering()); - $application->add(new \TestAmbiguousCommandRegistering2()); + $application->addCommand(new \TestAmbiguousCommandRegistering()); + $application->addCommand(new \TestAmbiguousCommandRegistering2()); $this->assertEquals('test-ambiguous', $application->find('test')->getName()); } @@ -364,9 +420,9 @@ public function testFindInvalidNamespace() public function testFindUniqueNameButNamespaceName() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); $this->expectException(CommandNotFoundException::class); $this->expectExceptionMessage('Command "foo1" is not defined'); @@ -377,7 +433,7 @@ public function testFindUniqueNameButNamespaceName() public function testFind() { $application = new Application(); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $this->assertInstanceOf(\FooCommand::class, $application->find('foo:bar'), '->find() returns a command if its name exists'); $this->assertInstanceOf(HelpCommand::class, $application->find('h'), '->find() returns a command if its name exists'); @@ -389,8 +445,8 @@ public function testFind() public function testFindCaseSensitiveFirst() { $application = new Application(); - $application->add(new \FooSameCaseUppercaseCommand()); - $application->add(new \FooSameCaseLowercaseCommand()); + $application->addCommand(new \FooSameCaseUppercaseCommand()); + $application->addCommand(new \FooSameCaseLowercaseCommand()); $this->assertInstanceOf(\FooSameCaseUppercaseCommand::class, $application->find('f:B'), '->find() returns a command if the abbreviation is the correct case'); $this->assertInstanceOf(\FooSameCaseUppercaseCommand::class, $application->find('f:BAR'), '->find() returns a command if the abbreviation is the correct case'); @@ -401,7 +457,7 @@ public function testFindCaseSensitiveFirst() public function testFindCaseInsensitiveAsFallback() { $application = new Application(); - $application->add(new \FooSameCaseLowercaseCommand()); + $application->addCommand(new \FooSameCaseLowercaseCommand()); $this->assertInstanceOf(\FooSameCaseLowercaseCommand::class, $application->find('f:b'), '->find() returns a command if the abbreviation is the correct case'); $this->assertInstanceOf(\FooSameCaseLowercaseCommand::class, $application->find('f:B'), '->find() will fallback to case insensitivity'); @@ -411,8 +467,8 @@ public function testFindCaseInsensitiveAsFallback() public function testFindCaseInsensitiveSuggestions() { $application = new Application(); - $application->add(new \FooSameCaseLowercaseCommand()); - $application->add(new \FooSameCaseUppercaseCommand()); + $application->addCommand(new \FooSameCaseLowercaseCommand()); + $application->addCommand(new \FooSameCaseUppercaseCommand()); $this->expectException(CommandNotFoundException::class); $this->expectExceptionMessage('Command "FoO:BaR" is ambiguous'); @@ -444,9 +500,9 @@ public function testFindWithAmbiguousAbbreviations($abbreviation, $expectedExcep $this->expectExceptionMessage($expectedExceptionMessage); $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); $application->find($abbreviation); } @@ -476,8 +532,8 @@ public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreH { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \FooHiddenCommand()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \FooHiddenCommand()); $this->assertInstanceOf(\FooCommand::class, $application->find('foo:')); } @@ -485,8 +541,8 @@ public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreH public function testFindCommandEqualNamespace() { $application = new Application(); - $application->add(new \Foo3Command()); - $application->add(new \Foo4Command()); + $application->addCommand(new \Foo3Command()); + $application->addCommand(new \Foo4Command()); $this->assertInstanceOf(\Foo3Command::class, $application->find('foo3:bar'), '->find() returns the good command even if a namespace has same name'); $this->assertInstanceOf(\Foo4Command::class, $application->find('foo3:bar:toh'), '->find() returns a command even if its namespace equals another command name'); @@ -495,8 +551,8 @@ public function testFindCommandEqualNamespace() public function testFindCommandWithAmbiguousNamespacesButUniqueName() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \FoobarCommand()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \FoobarCommand()); $this->assertInstanceOf(\FoobarCommand::class, $application->find('f:f')); } @@ -504,7 +560,7 @@ public function testFindCommandWithAmbiguousNamespacesButUniqueName() public function testFindCommandWithMissingNamespace() { $application = new Application(); - $application->add(new \Foo4Command()); + $application->addCommand(new \Foo4Command()); $this->assertInstanceOf(\Foo4Command::class, $application->find('f::t')); } @@ -515,7 +571,7 @@ public function testFindCommandWithMissingNamespace() public function testFindAlternativeExceptionMessageSingle($name) { $application = new Application(); - $application->add(new \Foo3Command()); + $application->addCommand(new \Foo3Command()); $this->expectException(CommandNotFoundException::class); $this->expectExceptionMessage('Did you mean this'); @@ -526,7 +582,7 @@ public function testFindAlternativeExceptionMessageSingle($name) public function testDontRunAlternativeNamespaceName() { $application = new Application(); - $application->add(new \Foo1Command()); + $application->addCommand(new \Foo1Command()); $application->setAutoExit(false); $tester = new ApplicationTester($application); $tester->run(['command' => 'foos:bar1'], ['decorated' => false]); @@ -536,7 +592,7 @@ public function testDontRunAlternativeNamespaceName() public function testCanRunAlternativeCommandName() { $application = new Application(); - $application->add(new \FooWithoutAliasCommand()); + $application->addCommand(new \FooWithoutAliasCommand()); $application->setAutoExit(false); $tester = new ApplicationTester($application); $tester->setInputs(['y']); @@ -550,7 +606,7 @@ public function testCanRunAlternativeCommandName() public function testDontRunAlternativeCommandName() { $application = new Application(); - $application->add(new \FooWithoutAliasCommand()); + $application->addCommand(new \FooWithoutAliasCommand()); $application->setAutoExit(false); $tester = new ApplicationTester($application); $tester->setInputs(['n']); @@ -574,9 +630,9 @@ public function testRunNamespace() putenv('COLUMNS=120'); $application = new Application(); $application->setAutoExit(false); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); $tester = new ApplicationTester($application); $tester->run(['command' => 'foo'], ['decorated' => false]); $display = trim($tester->getDisplay(true)); @@ -589,9 +645,9 @@ public function testFindAlternativeExceptionMessageMultiple() { putenv('COLUMNS=120'); $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); // Command + plural try { @@ -614,8 +670,8 @@ public function testFindAlternativeExceptionMessageMultiple() $this->assertMatchesRegularExpression('/foo1/', $e->getMessage()); } - $application->add(new \Foo3Command()); - $application->add(new \Foo4Command()); + $application->addCommand(new \Foo3Command()); + $application->addCommand(new \Foo4Command()); // Subnamespace + plural try { @@ -632,9 +688,9 @@ public function testFindAlternativeCommands() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); try { $application->find($commandName = 'Unknown command'); @@ -669,7 +725,7 @@ public function testFindAlternativeCommandsWithAnAlias() $application->setCommandLoader(new FactoryCommandLoader([ 'foo3' => static fn () => $fooCommand, ])); - $application->add($fooCommand); + $application->addCommand($fooCommand); $result = $application->find('foo'); @@ -680,10 +736,10 @@ public function testFindAlternativeNamespace() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); - $application->add(new \Foo3Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); + $application->addCommand(new \Foo3Command()); try { $application->find('Unknown-namespace:Unknown-command'); @@ -715,11 +771,11 @@ public function testFindAlternativesOutput() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo1Command()); - $application->add(new \Foo2Command()); - $application->add(new \Foo3Command()); - $application->add(new \FooHiddenCommand()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo1Command()); + $application->addCommand(new \Foo2Command()); + $application->addCommand(new \Foo3Command()); + $application->addCommand(new \FooHiddenCommand()); $expectedAlternatives = [ 'afoobar', @@ -755,8 +811,8 @@ public function testFindNamespaceDoesNotFailOnDeepSimilarNamespaces() public function testFindWithDoubleColonInNameThrowsException() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \Foo4Command()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \Foo4Command()); $this->expectException(CommandNotFoundException::class); $this->expectExceptionMessage('Command "foo::bar" is not defined.'); @@ -767,7 +823,7 @@ public function testFindWithDoubleColonInNameThrowsException() public function testFindHiddenWithExactName() { $application = new Application(); - $application->add(new \FooHiddenCommand()); + $application->addCommand(new \FooHiddenCommand()); $this->assertInstanceOf(\FooHiddenCommand::class, $application->find('foo:hidden')); $this->assertInstanceOf(\FooHiddenCommand::class, $application->find('afoohidden')); @@ -777,8 +833,8 @@ public function testFindAmbiguousCommandsIfAllAlternativesAreHidden() { $application = new Application(); - $application->add(new \FooCommand()); - $application->add(new \FooHiddenCommand()); + $application->addCommand(new \FooCommand()); + $application->addCommand(new \FooHiddenCommand()); $this->assertInstanceOf(\FooCommand::class, $application->find('foo:')); } @@ -824,7 +880,7 @@ public function testSetCatchErrors(bool $catchExceptions) $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions($catchExceptions); - $application->add((new Command('boom'))->setCode(fn () => throw new \Error('This is an error.'))); + $application->addCommand((new Command('boom'))->setCode(fn () => throw new \Error('This is an error.'))); putenv('COLUMNS=120'); $tester = new ApplicationTester($application); @@ -870,7 +926,7 @@ public function testRenderException() $tester->run(['command' => 'list', '--foo' => true], ['decorated' => false, 'capture_stderr_separately' => true]); $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getErrorOutput(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command'); - $application->add(new \Foo3Command()); + $application->addCommand(new \Foo3Command()); $tester = new ApplicationTester($application); $tester->run(['command' => 'foo3:bar'], ['decorated' => false, 'capture_stderr_separately' => true]); $this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception3.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exceptions with previous exceptions'); @@ -1031,7 +1087,7 @@ public function testRun() $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); - $application->add($command = new \Foo1Command()); + $application->addCommand($command = new \Foo1Command()); $_SERVER['argv'] = ['cli.php', 'foo:bar1']; ob_start(); @@ -1116,7 +1172,7 @@ public function testRun() $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $tester = new ApplicationTester($application); $tester->run(['command' => 'foo:bar', '--no-interaction' => true], ['decorated' => false]); @@ -1151,7 +1207,7 @@ public function testVerboseValueNotBreakArguments() $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $output = new StreamOutput(fopen('php://memory', 'w', false)); @@ -1762,7 +1818,7 @@ public function testSetRunCustomDefaultCommand() $application = new Application(); $application->setAutoExit(false); - $application->add($command); + $application->addCommand($command); $application->setDefaultCommand($command->getName()); $tester = new ApplicationTester($application); @@ -1784,7 +1840,7 @@ public function testSetRunCustomDefaultCommandWithOption() $application = new Application(); $application->setAutoExit(false); - $application->add($command); + $application->addCommand($command); $application->setDefaultCommand($command->getName()); $tester = new ApplicationTester($application); @@ -1799,7 +1855,7 @@ public function testSetRunCustomSingleCommand() $application = new Application(); $application->setAutoExit(false); - $application->add($command); + $application->addCommand($command); $application->setDefaultCommand($command->getName(), true); $tester = new ApplicationTester($application); @@ -2150,7 +2206,7 @@ public function testSignalableCommandInterfaceWithoutSignals() $application = new Application(); $application->setAutoExit(false); $application->setDispatcher($dispatcher); - $application->add($command); + $application->addCommand($command); $this->assertSame(0, $application->run(new ArrayInput(['signal']))); } @@ -2186,7 +2242,7 @@ public function testSignalableCommandDoesNotInterruptedOnTermSignals() $application = new Application(); $application->setAutoExit(false); $application->setDispatcher($dispatcher); - $application->add($command); + $application->addCommand($command); $this->assertSame(129, $application->run(new ArrayInput(['signal']))); } @@ -2208,7 +2264,7 @@ public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals() $application = new Application(); $application->setAutoExit(false); $application->setDispatcher($dispatcher); - $application->add($command); + $application->addCommand($command); $tester = new ApplicationTester($application); $this->assertSame(51, $tester->run(['signal'])); $expected = <<setAutoExit(false); $application->setDispatcher($dispatcher); - $application->add($command); + $application->addCommand($command); $this->assertSame(0, $application->run(new ArrayInput(['alarm']))); $this->assertFalse($command->signaled); @@ -2459,7 +2515,7 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI if ($dispatcher) { $application->setDispatcher($dispatcher); } - $application->add(new LazyCommand($command->getName(), [], '', false, fn () => $command, true)); + $application->addCommand(new LazyCommand($command->getName(), [], '', false, fn () => $command, true)); return $application; } @@ -2491,7 +2547,7 @@ public function __construct() parent::__construct(); $command = new \FooCommand(); - $this->add($command); + $this->addCommand($command); $this->setDefaultCommand($command->getName()); } } @@ -2514,6 +2570,22 @@ public function isEnabled(): bool } } +#[AsCommand(name: 'invokable')] +class InvokableTestCommand +{ + public function __invoke(): int + { + } +} + +#[AsCommand(name: 'invokable-extended')] +class InvokableExtendedTestCommand extends Command +{ + public function __invoke(): int + { + } +} + #[AsCommand(name: 'signal')] class BaseSignableCommand extends Command { diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index 85442c7b9..a3ecee43e 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -50,7 +50,7 @@ public function testCommandNameCannotBeEmpty() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('The command defined in "Symfony\Component\Console\Command\Command" cannot have an empty name.'); - (new Application())->add(new Command()); + (new Application())->addCommand(new Command()); } public function testSetApplication() @@ -190,7 +190,7 @@ public function testGetProcessedHelp() $command = new \TestCommand(); $command->setHelp('The %command.name% command does... Example: %command.full_name%.'); $application = new Application(); - $application->add($command); + $application->addCommand($command); $application->setDefaultCommand('namespace:name', true); $this->assertStringContainsString('The namespace:name command does...', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.name% correctly in single command applications'); $this->assertStringNotContainsString('%command.full_name%', $command->getProcessedHelp(), '->getProcessedHelp() replaces %command.full_name% in single command applications'); diff --git a/Tests/Command/CompleteCommandTest.php b/Tests/Command/CompleteCommandTest.php index 75519eb49..08f6b046f 100644 --- a/Tests/Command/CompleteCommandTest.php +++ b/Tests/Command/CompleteCommandTest.php @@ -33,7 +33,7 @@ protected function setUp(): void $this->command = new CompleteCommand(); $this->application = new Application(); - $this->application->add(new CompleteCommandTest_HelloCommand()); + $this->application->addCommand(new CompleteCommandTest_HelloCommand()); $this->command->setApplication($this->application); $this->tester = new CommandTester($this->command); diff --git a/Tests/Command/HelpCommandTest.php b/Tests/Command/HelpCommandTest.php index c36ab62df..f1979c0dc 100644 --- a/Tests/Command/HelpCommandTest.php +++ b/Tests/Command/HelpCommandTest.php @@ -77,7 +77,7 @@ public function testComplete(array $input, array $expectedSuggestions) { require_once realpath(__DIR__.'/../Fixtures/FooCommand.php'); $application = new Application(); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $tester = new CommandCompletionTester($application->get('help')); $suggestions = $tester->complete($input, 2); $this->assertSame($expectedSuggestions, $suggestions); diff --git a/Tests/Command/ListCommandTest.php b/Tests/Command/ListCommandTest.php index a6ffc8ab5..37496c6b3 100644 --- a/Tests/Command/ListCommandTest.php +++ b/Tests/Command/ListCommandTest.php @@ -54,7 +54,7 @@ public function testExecuteListsCommandsWithNamespaceArgument() { require_once realpath(__DIR__.'/../Fixtures/FooCommand.php'); $application = new Application(); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), 'namespace' => 'foo', '--raw' => true]); $output = <<<'EOF' @@ -69,7 +69,7 @@ public function testExecuteListsCommandsOrder() { require_once realpath(__DIR__.'/../Fixtures/Foo6Command.php'); $application = new Application(); - $application->add(new \Foo6Command()); + $application->addCommand(new \Foo6Command()); $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName()], ['decorated' => false]); $output = <<<'EOF' @@ -102,7 +102,7 @@ public function testExecuteListsCommandsOrderRaw() { require_once realpath(__DIR__.'/../Fixtures/Foo6Command.php'); $application = new Application(); - $application->add(new \Foo6Command()); + $application->addCommand(new \Foo6Command()); $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), '--raw' => true]); $output = <<<'EOF' @@ -122,7 +122,7 @@ public function testComplete(array $input, array $expectedSuggestions) { require_once realpath(__DIR__.'/../Fixtures/FooCommand.php'); $application = new Application(); - $application->add(new \FooCommand()); + $application->addCommand(new \FooCommand()); $tester = new CommandCompletionTester($application->get('list')); $suggestions = $tester->complete($input, 2); $this->assertSame($expectedSuggestions, $suggestions); diff --git a/Tests/ConsoleEventsTest.php b/Tests/ConsoleEventsTest.php index 408f8c0d3..3421eda80 100644 --- a/Tests/ConsoleEventsTest.php +++ b/Tests/ConsoleEventsTest.php @@ -58,7 +58,7 @@ public function testEventAliases() ->setPublic(true) ->addMethodCall('setAutoExit', [false]) ->addMethodCall('setDispatcher', [new Reference('event_dispatcher')]) - ->addMethodCall('add', [new Reference('failing_command')]) + ->addMethodCall('addCommand', [new Reference('failing_command')]) ; $container->compile(); diff --git a/Tests/Descriptor/ApplicationDescriptionTest.php b/Tests/Descriptor/ApplicationDescriptionTest.php index 1933c985c..ab90320cd 100644 --- a/Tests/Descriptor/ApplicationDescriptionTest.php +++ b/Tests/Descriptor/ApplicationDescriptionTest.php @@ -25,7 +25,7 @@ public function testGetNamespaces(array $expected, array $names) { $application = new TestApplication(); foreach ($names as $name) { - $application->add(new Command($name)); + $application->addCommand(new Command($name)); } $this->assertSame($expected, array_keys((new ApplicationDescription($application))->getNamespaces())); diff --git a/Tests/Fixtures/DescriptorApplication2.php b/Tests/Fixtures/DescriptorApplication2.php index 7bb02fa54..c755bab38 100644 --- a/Tests/Fixtures/DescriptorApplication2.php +++ b/Tests/Fixtures/DescriptorApplication2.php @@ -18,9 +18,9 @@ class DescriptorApplication2 extends Application public function __construct() { parent::__construct('My Symfony application', 'v1.0'); - $this->add(new DescriptorCommand1()); - $this->add(new DescriptorCommand2()); - $this->add(new DescriptorCommand3()); - $this->add(new DescriptorCommand4()); + $this->addCommand(new DescriptorCommand1()); + $this->addCommand(new DescriptorCommand2()); + $this->addCommand(new DescriptorCommand3()); + $this->addCommand(new DescriptorCommand4()); } } diff --git a/Tests/Fixtures/DescriptorApplicationMbString.php b/Tests/Fixtures/DescriptorApplicationMbString.php index bf170c449..a76e0e181 100644 --- a/Tests/Fixtures/DescriptorApplicationMbString.php +++ b/Tests/Fixtures/DescriptorApplicationMbString.php @@ -19,6 +19,6 @@ public function __construct() { parent::__construct('MbString åpplicätion'); - $this->add(new DescriptorCommandMbString()); + $this->addCommand(new DescriptorCommandMbString()); } } diff --git a/Tests/Tester/CommandTesterTest.php b/Tests/Tester/CommandTesterTest.php index cfdebe4d8..d1fb20ac5 100644 --- a/Tests/Tester/CommandTesterTest.php +++ b/Tests/Tester/CommandTesterTest.php @@ -104,7 +104,7 @@ public function testCommandFromApplication() return 0; }); - $application->add($command); + $application->addCommand($command); $tester = new CommandTester($application->find('foo')); diff --git a/Tests/phpt/alarm/command_exit.phpt b/Tests/phpt/alarm/command_exit.phpt index c2cf3edc7..a53af8567 100644 --- a/Tests/phpt/alarm/command_exit.phpt +++ b/Tests/phpt/alarm/command_exit.phpt @@ -53,7 +53,7 @@ class MyCommand extends Command $app = new Application(); $app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher()); -$app->add(new MyCommand('foo')); +$app->addCommand(new MyCommand('foo')); $app ->setDefaultCommand('foo', true) diff --git a/Tests/phpt/signal/command_exit.phpt b/Tests/phpt/signal/command_exit.phpt index e14f80c47..e653d65c1 100644 --- a/Tests/phpt/signal/command_exit.phpt +++ b/Tests/phpt/signal/command_exit.phpt @@ -45,7 +45,7 @@ class MyCommand extends Command $app = new Application(); $app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher()); -$app->add(new MyCommand('foo')); +$app->addCommand(new MyCommand('foo')); $app ->setDefaultCommand('foo', true) From 40799aa45ae5ede3e22f41ce0ddc702ef19fa991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 26 May 2025 11:00:21 +0200 Subject: [PATCH 05/36] [Console] Support enum in invokable commands Co-authored-by: Nicolas Grekas --- Attribute/Argument.php | 27 ++++++-- Attribute/Option.php | 16 ++++- CHANGELOG.md | 1 + Exception/InvalidArgumentException.php | 13 ++++ Exception/InvalidOptionException.php | 13 ++++ Tests/Command/InvokableCommandTest.php | 90 ++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 9 deletions(-) diff --git a/Attribute/Argument.php b/Attribute/Argument.php index e6a94d2f1..f2c813d3b 100644 --- a/Attribute/Argument.php +++ b/Attribute/Argument.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -27,6 +28,7 @@ class Argument private array|\Closure $suggestedValues; private ?int $mode = null; private string $function = ''; + private string $typeName = ''; /** * Represents a console command definition. @@ -66,20 +68,23 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name, $self->function)); } - $parameterTypeName = $type->getName(); + $self->typeName = $type->getName(); + $isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class); - if (!\in_array($parameterTypeName, self::ALLOWED_TYPES, true)) { - throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); + if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) { + throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); } if (!$self->name) { $self->name = (new UnicodeString($name))->kebab(); } - $self->default = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + if ($parameter->isDefaultValueAvailable()) { + $self->default = $parameter->getDefaultValue() instanceof \BackedEnum ? $parameter->getDefaultValue()->value : $parameter->getDefaultValue(); + } $self->mode = $parameter->isDefaultValueAvailable() || $parameter->allowsNull() ? InputArgument::OPTIONAL : InputArgument::REQUIRED; - if ('array' === $parameterTypeName) { + if ('array' === $self->typeName) { $self->mode |= InputArgument::IS_ARRAY; } @@ -87,6 +92,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self $self->suggestedValues = [$instance, $self->suggestedValues[1]]; } + if ($isBackedEnum && !$self->suggestedValues) { + $self->suggestedValues = array_column(($self->typeName)::cases(), 'value'); + } + return $self; } @@ -105,6 +114,12 @@ public function toInputArgument(): InputArgument */ public function resolveValue(InputInterface $input): mixed { - return $input->getArgument($this->name); + $value = $input->getArgument($this->name); + + if (is_subclass_of($this->typeName, \BackedEnum::class) && (is_string($value) || is_int($value))) { + return ($this->typeName)::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues); + } + + return $value; } } diff --git a/Attribute/Option.php b/Attribute/Option.php index 2f0256b17..8065d6ad8 100644 --- a/Attribute/Option.php +++ b/Attribute/Option.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -75,7 +76,7 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self $self->name = (new UnicodeString($name))->kebab(); } - $self->default = $parameter->getDefaultValue(); + $self->default = $parameter->getDefaultValue() instanceof \BackedEnum ? $parameter->getDefaultValue()->value : $parameter->getDefaultValue(); $self->allowNull = $parameter->allowsNull(); if ($type instanceof \ReflectionUnionType) { @@ -87,9 +88,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self } $self->typeName = $type->getName(); + $isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class); - if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) { - throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); + if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) { + throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types and BackedEnum are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); } if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) { @@ -115,6 +117,10 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self $self->suggestedValues = [$instance, $self->suggestedValues[1]]; } + if ($isBackedEnum && !$self->suggestedValues) { + $self->suggestedValues = array_column(($self->typeName)::cases(), 'value'); + } + return $self; } @@ -140,6 +146,10 @@ public function resolveValue(InputInterface $input): mixed return true; } + if (is_subclass_of($this->typeName, \BackedEnum::class) && (is_string($value) || is_int($value))) { + return ($this->typeName)::tryFrom($value) ?? throw InvalidOptionException::fromEnumValue($this->name, $value, $this->suggestedValues); + } + if ('array' === $this->typeName && $this->allowNull && [] === $value) { return null; } diff --git a/CHANGELOG.md b/CHANGELOG.md index f481d55aa..f5e15ade7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Allow setting aliases and the hidden flag via the command name passed to the constructor * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` + * Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands 7.3 --- diff --git a/Exception/InvalidArgumentException.php b/Exception/InvalidArgumentException.php index 07cc0b61d..0482244f2 100644 --- a/Exception/InvalidArgumentException.php +++ b/Exception/InvalidArgumentException.php @@ -16,4 +16,17 @@ */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { + /** + * @internal + */ + public static function fromEnumValue(string $name, string $value, array|\Closure $suggestedValues): self + { + $error = \sprintf('The value "%s" is not valid for the "%s" argument.', $value, $name); + + if (\is_array($suggestedValues)) { + $error .= \sprintf(' Supported values are "%s".', implode('", "', $suggestedValues)); + } + + return new self($error); + } } diff --git a/Exception/InvalidOptionException.php b/Exception/InvalidOptionException.php index 5cf62792e..e59167df1 100644 --- a/Exception/InvalidOptionException.php +++ b/Exception/InvalidOptionException.php @@ -18,4 +18,17 @@ */ class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface { + /** + * @internal + */ + public static function fromEnumValue(string $name, string $value, array|\Closure $suggestedValues): self + { + $error = \sprintf('The value "%s" is not valid for the "%s" option.', $value, $name); + + if (\is_array($suggestedValues)) { + $error .= \sprintf(' Supported values are "%s".', implode('", "', $suggestedValues)); + } + + return new self($error); + } } diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php index 5ab7951e7..785891586 100644 --- a/Tests/Command/InvokableCommandTest.php +++ b/Tests/Command/InvokableCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\Option; @@ -18,6 +19,7 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\ArrayInput; @@ -132,6 +134,88 @@ public function testCommandInputOptionDefinition() self::assertFalse($optInputOption->getDefault()); } + public function testEnumArgument() + { + $command = new Command('foo'); + $command->setCode(function ( + #[Argument] StringEnum $enum, + #[Argument] StringEnum $enumWithDefault = StringEnum::Image, + #[Argument] ?StringEnum $nullableEnum = null, + ): int { + Assert::assertSame(StringEnum::Image, $enum); + Assert::assertSame(StringEnum::Image, $enumWithDefault); + Assert::assertNull($nullableEnum); + + return 0; + }); + + $enumInputArgument = $command->getDefinition()->getArgument('enum'); + self::assertTrue($enumInputArgument->isRequired()); + self::assertNull($enumInputArgument->getDefault()); + self::assertTrue($enumInputArgument->hasCompletion()); + + $enumWithDefaultInputArgument = $command->getDefinition()->getArgument('enum-with-default'); + self::assertFalse($enumWithDefaultInputArgument->isRequired()); + self::assertSame('image', $enumWithDefaultInputArgument->getDefault()); + self::assertTrue($enumWithDefaultInputArgument->hasCompletion()); + + $nullableEnumInputArgument = $command->getDefinition()->getArgument('nullable-enum'); + self::assertFalse($nullableEnumInputArgument->isRequired()); + self::assertNull($nullableEnumInputArgument->getDefault()); + self::assertTrue($nullableEnumInputArgument->hasCompletion()); + + $enumInputArgument->complete(CompletionInput::fromTokens([], 0), $suggestions = new CompletionSuggestions()); + self::assertEquals([new Suggestion('image'), new Suggestion('video')], $suggestions->getValueSuggestions()); + + $command->run(new ArrayInput(['enum' => 'image']), new NullOutput()); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('The value "incorrect" is not valid for the "enum" argument. Supported values are "image", "video".'); + + $command->run(new ArrayInput(['enum' => 'incorrect']), new NullOutput()); + } + + public function testEnumOption() + { + $command = new Command('foo'); + $command->setCode(function ( + #[Option] StringEnum $enum = StringEnum::Video, + #[Option] StringEnum $enumWithDefault = StringEnum::Image, + #[Option] ?StringEnum $nullableEnum = null, + ): int { + Assert::assertSame(StringEnum::Image, $enum); + Assert::assertSame(StringEnum::Image, $enumWithDefault); + Assert::assertNull($nullableEnum); + + return 0; + }); + + $enumInputOption = $command->getDefinition()->getOption('enum'); + self::assertTrue($enumInputOption->isValueRequired()); + self::assertSame('video', $enumInputOption->getDefault()); + self::assertTrue($enumInputOption->hasCompletion()); + + $enumWithDefaultInputOption = $command->getDefinition()->getOption('enum-with-default'); + self::assertTrue($enumWithDefaultInputOption->isValueRequired()); + self::assertSame('image', $enumWithDefaultInputOption->getDefault()); + self::assertTrue($enumWithDefaultInputOption->hasCompletion()); + + $nullableEnumInputOption = $command->getDefinition()->getOption('nullable-enum'); + self::assertTrue($nullableEnumInputOption->isValueRequired()); + self::assertNull($nullableEnumInputOption->getDefault()); + self::assertTrue($nullableEnumInputOption->hasCompletion()); + + $enumInputOption->complete(CompletionInput::fromTokens([], 0), $suggestions = new CompletionSuggestions()); + self::assertEquals([new Suggestion('image'), new Suggestion('video')], $suggestions->getValueSuggestions()); + + $command->run(new ArrayInput(['--enum' => 'image']), new NullOutput()); + + self::expectException(InvalidOptionException::class); + self::expectExceptionMessage('The value "incorrect" is not valid for the "enum" option. Supported values are "image", "video".'); + + $command->run(new ArrayInput(['--enum' => 'incorrect']), new NullOutput()); + } + public function testInvalidArgumentType() { $command = new Command('foo'); @@ -377,3 +461,9 @@ public function getSuggestedRoles(CompletionInput $input): array return ['ROLE_ADMIN', 'ROLE_USER']; } } + +enum StringEnum: string +{ + case Image = 'image'; + case Video = 'video'; +} From 13cff4946131b09c4e44c69e572fa77fbe6e9bd7 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 11 Jun 2025 22:47:06 -0400 Subject: [PATCH 06/36] [Console] Allow Usages to be specified via #[AsCommand] --- Application.php | 4 ++++ Attribute/AsCommand.php | 2 ++ CHANGELOG.md | 1 + Command/Command.php | 4 ++++ DependencyInjection/AddConsoleCommandPass.php | 8 ++++++++ Tests/Command/CommandTest.php | 4 +++- Tests/DependencyInjection/AddConsoleCommandPassTest.php | 3 +++ 7 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Application.php b/Application.php index f77d57299..20fa7870a 100644 --- a/Application.php +++ b/Application.php @@ -564,6 +564,10 @@ public function addCommand(callable|Command $command): ?Command ->setDescription($attribute->description ?? '') ->setHelp($attribute->help ?? '') ->setCode($command); + + foreach ($attribute->usages as $usage) { + $command->addUsage($usage); + } } $command->setApplication($this); diff --git a/Attribute/AsCommand.php b/Attribute/AsCommand.php index 767d46ebb..02f156201 100644 --- a/Attribute/AsCommand.php +++ b/Attribute/AsCommand.php @@ -25,6 +25,7 @@ class AsCommand * @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean") * @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command * @param string|null $help The help content of the command, displayed with the help page + * @param string[] $usages The list of usage examples, displayed with the help page */ public function __construct( public string $name, @@ -32,6 +33,7 @@ public function __construct( array $aliases = [], bool $hidden = false, public ?string $help = null, + public array $usages = [], ) { if (!$hidden && !$aliases) { return; diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e15ade7..1922e6562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` * Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands + * Allow Usages to be specified via #[AsCommand] attribute. 7.3 --- diff --git a/Command/Command.php b/Command/Command.php index 23e3b6621..0ae82cf9a 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -134,6 +134,10 @@ public function __construct(?string $name = null) $this->setHelp($attribute?->help ?? ''); } + foreach ($attribute?->usages ?? [] as $usage) { + $this->addUsage($usage); + } + if (\is_callable($this) && (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name === self::class) { $this->code = new InvokableCommand($this, $this(...)); } diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php index 562627f4b..4a0ee4229 100644 --- a/DependencyInjection/AddConsoleCommandPass.php +++ b/DependencyInjection/AddConsoleCommandPass.php @@ -91,6 +91,7 @@ public function process(ContainerBuilder $container): void $description = $tags[0]['description'] ?? null; $help = $tags[0]['help'] ?? null; + $usages = $tags[0]['usages'] ?? null; unset($tags[0]); $lazyCommandMap[$commandName] = $id; @@ -108,6 +109,7 @@ public function process(ContainerBuilder $container): void $description ??= $tag['description'] ?? null; $help ??= $tag['help'] ?? null; + $usages ??= $tag['usages'] ?? null; } $definition->addMethodCall('setName', [$commandName]); @@ -124,6 +126,12 @@ public function process(ContainerBuilder $container): void $definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]); } + if ($usages) { + foreach ($usages as $usage) { + $definition->addMethodCall('addUsage', [$usage]); + } + } + if (!$description) { if (Command::class !== (new \ReflectionMethod($class, 'getDefaultDescription'))->class) { trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultDescription()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', $class); diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index a3ecee43e..44e899629 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -457,6 +457,8 @@ public function testCommandAttribute() $this->assertSame('foo', $command->getName()); $this->assertSame('desc', $command->getDescription()); $this->assertSame('help', $command->getHelp()); + $this->assertCount(2, $command->getUsages()); + $this->assertStringContainsString('usage1', $command->getUsages()[0]); $this->assertTrue($command->isHidden()); $this->assertSame(['f'], $command->getAliases()); } @@ -542,7 +544,7 @@ function createClosure() }; } -#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'], help: 'help')] +#[AsCommand(name: 'foo', description: 'desc', usages: ['usage1', 'usage2'], hidden: true, aliases: ['f'], help: 'help')] class Php8Command extends Command { } diff --git a/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/Tests/DependencyInjection/AddConsoleCommandPassTest.php index 9ac660100..a11e6b510 100644 --- a/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -315,6 +315,7 @@ public function testProcessInvokableCommand() $definition->addTag('console.command', [ 'command' => 'invokable', 'description' => 'The command description', + 'usages' => ['usage1', 'usage2'], 'help' => 'The %command.name% command help content.', ]); $container->setDefinition('invokable_command', $definition); @@ -325,6 +326,8 @@ public function testProcessInvokableCommand() self::assertTrue($container->has('invokable_command.command')); self::assertSame('The command description', $command->getDescription()); self::assertSame('The %command.name% command help content.', $command->getHelp()); + self::assertCount(2, $command->getUsages()); + $this->assertStringContainsString('usage1', $command->getUsages()[0]); } public function testProcessInvokableSignalableCommand() From d6fb3f949edcfb1cd404b91b03ef88815f4dfd4f Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Mon, 9 Jun 2025 08:20:00 -0400 Subject: [PATCH 07/36] Improve-callable-typing --- Helper/QuestionHelper.php | 3 ++- Question/Question.php | 21 +++++++++++++++++++-- Style/StyleInterface.php | 4 ++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 8e1591ec1..9b65c3213 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -234,7 +234,8 @@ protected function writeError(OutputInterface $output, \Exception $error): void /** * Autocompletes a question. * - * @param resource $inputStream + * @param resource $inputStream + * @param callable(string):string[] $autocomplete */ private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string { diff --git a/Question/Question.php b/Question/Question.php index 46a60c798..cb65bd674 100644 --- a/Question/Question.php +++ b/Question/Question.php @@ -24,8 +24,17 @@ class Question private ?int $attempts = null; private bool $hidden = false; private bool $hiddenFallback = true; + /** + * @var (\Closure(string):string[])|null + */ private ?\Closure $autocompleterCallback = null; + /** + * @var (\Closure(mixed):mixed)|null + */ private ?\Closure $validator = null; + /** + * @var (\Closure(mixed):mixed)|null + */ private ?\Closure $normalizer = null; private bool $trimmable = true; private bool $multiline = false; @@ -160,6 +169,8 @@ public function setAutocompleterValues(?iterable $values): static /** * Gets the callback function used for the autocompleter. + * + * @return (callable(string):string[])|null */ public function getAutocompleterCallback(): ?callable { @@ -171,6 +182,8 @@ public function getAutocompleterCallback(): ?callable * * The callback is passed the user input as argument and should return an iterable of corresponding suggestions. * + * @param (callable(string):string[])|null $callback + * * @return $this */ public function setAutocompleterCallback(?callable $callback): static @@ -187,6 +200,8 @@ public function setAutocompleterCallback(?callable $callback): static /** * Sets a validator for the question. * + * @param (callable(mixed):mixed)|null $validator + * * @return $this */ public function setValidator(?callable $validator): static @@ -198,6 +213,8 @@ public function setValidator(?callable $validator): static /** * Gets the validator for the question. + * + * @return (callable(mixed):mixed)|null */ public function getValidator(): ?callable { @@ -237,7 +254,7 @@ public function getMaxAttempts(): ?int /** * Sets a normalizer for the response. * - * The normalizer can be a callable (a string), a closure or a class implementing __invoke. + * @param callable(mixed):mixed $normalizer * * @return $this */ @@ -251,7 +268,7 @@ public function setNormalizer(callable $normalizer): static /** * Gets the normalizer for the response. * - * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * @return (callable(mixed):mixed)|null */ public function getNormalizer(): ?callable { diff --git a/Style/StyleInterface.php b/Style/StyleInterface.php index fcc5bc775..1a2232324 100644 --- a/Style/StyleInterface.php +++ b/Style/StyleInterface.php @@ -70,11 +70,15 @@ public function table(array $headers, array $rows): void; /** * Asks a question. + * + * @param (callable(mixed):mixed)|null $validator */ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed; /** * Asks a question with the user input hidden. + * + * @param (callable(mixed):mixed)|null $validator */ public function askHidden(string $question, ?callable $validator = null): mixed; From 969df4bcdb211680a7463b0c22484c8fa63dc3b0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 15 Jun 2025 21:39:02 +0200 Subject: [PATCH 08/36] fix backwards-compatibility with overridden add() methods --- Application.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Application.php b/Application.php index 20fa7870a..7489dab79 100644 --- a/Application.php +++ b/Application.php @@ -1352,8 +1352,14 @@ private function init(): void } $this->initialized = true; + if ((new \ReflectionMethod($this, 'add'))->getDeclaringClass()->getName() !== (new \ReflectionMethod($this, 'addCommand'))->getDeclaringClass()->getName()) { + $adder = $this->add(...); + } else { + $adder = $this->addCommand(...); + } + foreach ($this->getDefaultCommands() as $command) { - $this->addCommand($command); + $adder($command); } } } From 3da24a8f8592670bb7c1d818339eaa46811bd238 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Wed, 18 Jun 2025 02:51:09 +0200 Subject: [PATCH 09/36] Fix code example in PHPDoc block --- Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application.php b/Application.php index 7489dab79..6034b78f5 100644 --- a/Application.php +++ b/Application.php @@ -67,7 +67,7 @@ * Usage: * * $app = new Application('myapp', '1.0 (stable)'); - * $app->add(new SimpleCommand()); + * $app->addCommand(new SimpleCommand()); * $app->run(); * * @author Fabien Potencier From 63e8458de1742829d278710b54392a1dfbfd92ca Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 20 Jun 2025 09:25:10 +0200 Subject: [PATCH 10/36] Add support for Invokable Commands in CommandTester --- Application.php | 17 +------------- CHANGELOG.md | 3 ++- Command/Command.php | 23 ++++++++++++++++++- Tester/CommandTester.php | 5 +++- Tests/ApplicationTest.php | 12 +++------- Tests/Command/CommandTest.php | 8 +++++++ .../InvokableExtendingCommandTestCommand.php | 15 ++++++++++++ Tests/Fixtures/InvokableTestCommand.php | 15 ++++++++++++ Tests/Tester/CommandTesterTest.php | 22 ++++++++++++++++++ 9 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 Tests/Fixtures/InvokableExtendingCommandTestCommand.php create mode 100644 Tests/Fixtures/InvokableTestCommand.php diff --git a/Application.php b/Application.php index 6034b78f5..fa3c381cf 100644 --- a/Application.php +++ b/Application.php @@ -552,22 +552,7 @@ public function addCommand(callable|Command $command): ?Command $this->init(); if (!$command instanceof Command) { - if (!\is_object($command) || $command instanceof \Closure) { - throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)); - } - - /** @var AsCommand $attribute */ - $attribute = ((new \ReflectionObject($command))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance() - ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class)); - - $command = (new Command($attribute->name)) - ->setDescription($attribute->description ?? '') - ->setHelp($attribute->help ?? '') - ->setCode($command); - - foreach ($attribute->usages as $usage) { - $command->addUsage($usage); - } + $command = new Command(null, $command); } $command->setApplication($this); diff --git a/CHANGELOG.md b/CHANGELOG.md index 1922e6562..722045091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ CHANGELOG * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` * Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands - * Allow Usages to be specified via #[AsCommand] attribute. + * Allow Usages to be specified via `#[AsCommand]` attribute. + * Allow passing invokable commands to `Symfony\Component\Console\Tester\CommandTester` 7.3 --- diff --git a/Command/Command.php b/Command/Command.php index 0ae82cf9a..9e6e41ec9 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -87,10 +87,31 @@ public static function getDefaultDescription(): ?string * * @throws LogicException When the command name is empty */ - public function __construct(?string $name = null) + public function __construct(?string $name = null, ?callable $code = null) { $this->definition = new InputDefinition(); + if ($code !== null) { + if (!\is_object($code) || $code instanceof \Closure) { + throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)); + } + + /** @var AsCommand $attribute */ + $attribute = ((new \ReflectionObject($code))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance() + ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class)); + + $this->setName($name ?? $attribute->name) + ->setDescription($attribute->description ?? '') + ->setHelp($attribute->help ?? '') + ->setCode($code); + + foreach ($attribute->usages as $usage) { + $this->addUsage($usage); + } + + return; + } + $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); if (null === $name) { diff --git a/Tester/CommandTester.php b/Tester/CommandTester.php index d39cde7f6..714d88ad5 100644 --- a/Tester/CommandTester.php +++ b/Tester/CommandTester.php @@ -24,9 +24,12 @@ class CommandTester { use TesterTrait; + private Command $command; + public function __construct( - private Command $command, + callable|Command $command, ) { + $this->command = $command instanceof Command ? $command : new Command(null, $command); } /** diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index e5d16d7fe..1a730a95b 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -48,6 +48,8 @@ use Symfony\Component\Console\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Terminal; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Tests\Fixtures\InvokableExtendingCommandTestCommand; +use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; use Symfony\Component\Console\Tests\Fixtures\MockableAppliationWithTerminalWidth; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -257,7 +259,7 @@ public function testAddCommandWithInvokableCommand() $application->addCommand($foo = new InvokableTestCommand()); $commands = $application->all(); - $this->assertInstanceOf(Command::class, $command = $commands['invokable']); + $this->assertInstanceOf(Command::class, $command = $commands['invokable:test']); $this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command)); } @@ -2570,14 +2572,6 @@ public function isEnabled(): bool } } -#[AsCommand(name: 'invokable')] -class InvokableTestCommand -{ - public function __invoke(): int - { - } -} - #[AsCommand(name: 'invokable-extended')] class InvokableExtendedTestCommand extends Command { diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index 44e899629..a4a719b3d 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; class CommandTest extends TestCase { @@ -304,6 +305,13 @@ public function testRunInteractive() $this->assertEquals('interact called'.\PHP_EOL.'execute called'.\PHP_EOL, $tester->getDisplay(), '->run() calls the interact() method if the input is interactive'); } + public function testInvokableCommand() + { + $tester = new CommandTester(new InvokableTestCommand()); + + $this->assertSame(Command::SUCCESS, $tester->execute([])); + } + public function testRunNonInteractive() { $tester = new CommandTester(new \TestCommand()); diff --git a/Tests/Fixtures/InvokableExtendingCommandTestCommand.php b/Tests/Fixtures/InvokableExtendingCommandTestCommand.php new file mode 100644 index 000000000..724951608 --- /dev/null +++ b/Tests/Fixtures/InvokableExtendingCommandTestCommand.php @@ -0,0 +1,15 @@ +assertSame('foo', $tester->getErrorOutput()); } + + public function testAInvokableCommand() + { + $command = new InvokableTestCommand(); + + $tester = new CommandTester($command); + $tester->execute([]); + + $tester->assertCommandIsSuccessful(); + } + + public function testAInvokableExtendedCommand() + { + $command = new InvokableExtendingCommandTestCommand(); + + $tester = new CommandTester($command); + $tester->execute([]); + + $tester->assertCommandIsSuccessful(); + } } From 6a0d19c7a69f7d8a011ac7084c932bfc07525e30 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Tue, 24 Jun 2025 02:03:54 +0200 Subject: [PATCH 11/36] [Console] Cleanup test --- Tests/ApplicationTest.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 1a730a95b..e9b45c051 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -266,10 +266,10 @@ public function testAddCommandWithInvokableCommand() public function testAddCommandWithInvokableExtendedCommand() { $application = new Application(); - $application->addCommand($foo = new InvokableExtendedTestCommand()); + $application->addCommand($foo = new InvokableExtendingCommandTestCommand()); $commands = $application->all(); - $this->assertEquals($foo, $commands['invokable-extended']); + $this->assertEquals($foo, $commands['invokable:test']); } /** @@ -2572,14 +2572,6 @@ public function isEnabled(): bool } } -#[AsCommand(name: 'invokable-extended')] -class InvokableExtendedTestCommand extends Command -{ - public function __invoke(): int - { - } -} - #[AsCommand(name: 'signal')] class BaseSignableCommand extends Command { From c15844668e56e0be8b16c2ed289ad0ff6e5f7b08 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 27 Jun 2025 23:42:21 +0200 Subject: [PATCH 12/36] Fix typos in documentation and code comments --- Tests/ApplicationTest.php | 2 +- Tests/Helper/TableTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index e9b45c051..27c94a816 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -988,7 +988,7 @@ public function testRenderExceptionEscapesLines() $application->setAutoExit(false); putenv('COLUMNS=22'); $application->register('foo')->setCode(function () { - throw new \Exception('dont break here !'); + throw new \Exception('don\'t break here !'); }); $tester = new ApplicationTester($application); diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 52ae23301..131c6f522 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -616,12 +616,12 @@ public static function renderProvider() [], [ [ - new TableCell('Dont break'."\n".'here', ['colspan' => 2]), + new TableCell('Don\'t break'."\n".'here', ['colspan' => 2]), ], new TableSeparator(), [ 'foo', - new TableCell('Dont break'."\n".'here', ['rowspan' => 2]), + new TableCell('Don\'t break'."\n".'here', ['rowspan' => 2]), ], [ 'bar', From 1ac16d431edc1978c29a462e0589e49e1fa4c928 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 28 Jun 2025 10:02:13 +0200 Subject: [PATCH 13/36] Fix tests --- Tests/ApplicationTest.php | 2 +- Tests/Helper/TableTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 27c94a816..e9b45c051 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -988,7 +988,7 @@ public function testRenderExceptionEscapesLines() $application->setAutoExit(false); putenv('COLUMNS=22'); $application->register('foo')->setCode(function () { - throw new \Exception('don\'t break here !'); + throw new \Exception('dont break here !'); }); $tester = new ApplicationTester($application); diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 131c6f522..52ae23301 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -616,12 +616,12 @@ public static function renderProvider() [], [ [ - new TableCell('Don\'t break'."\n".'here', ['colspan' => 2]), + new TableCell('Dont break'."\n".'here', ['colspan' => 2]), ], new TableSeparator(), [ 'foo', - new TableCell('Don\'t break'."\n".'here', ['rowspan' => 2]), + new TableCell('Dont break'."\n".'here', ['rowspan' => 2]), ], [ 'bar', From 454e9b80987bebab3d7b9f4c0c3f81593d99ae5b Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sat, 5 Jul 2025 15:43:06 +0200 Subject: [PATCH 14/36] chore: PHP CS Fixer fixes --- Tests/Command/InvokableCommandTest.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php index 5ab7951e7..ef8059a5e 100644 --- a/Tests/Command/InvokableCommandTest.php +++ b/Tests/Command/InvokableCommandTest.php @@ -179,6 +179,7 @@ public function testCallInvokeMethodWhenExtendingCommandClass() { $command = new class extends Command { public string $called; + public function __invoke(): int { $this->called = __FUNCTION__; @@ -195,7 +196,9 @@ public function testInvalidReturnType() { $command = new Command('foo'); $command->setCode(new class { - public function __invoke() {} + public function __invoke() + { + } }); $this->expectException(\TypeError::class); @@ -333,16 +336,16 @@ public function testInvalidOptionDefinition(callable $code) public static function provideInvalidOptionDefinitions(): \Generator { yield 'no-default' => [ - function (#[Option] string $a) {} + function (#[Option] string $a) {}, ]; yield 'nullable-bool-default-true' => [ - function (#[Option] ?bool $a = true) {} + function (#[Option] ?bool $a = true) {}, ]; yield 'nullable-bool-default-false' => [ - function (#[Option] ?bool $a = false) {} + function (#[Option] ?bool $a = false) {}, ]; yield 'invalid-union-type' => [ - function (#[Option] array|bool $a = false) {} + function (#[Option] array|bool $a = false) {}, ]; yield 'union-type-cannot-allow-null' => [ function (#[Option] string|bool|null $a = null) {}, From 7ad7fdc79778358d2a2da368cd183dbb7ccd94a6 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 8 Jul 2025 11:08:29 +0200 Subject: [PATCH 15/36] Various CS fixes --- Application.php | 2 -- Attribute/Argument.php | 2 +- Attribute/Option.php | 4 ++-- Command/Command.php | 6 +++--- Tests/Command/InvokableCommandTest.php | 1 + Tests/Helper/ProgressBarTest.php | 18 +++++++++--------- Tests/Helper/TableTest.php | 8 ++++---- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Application.php b/Application.php index 92efbce84..47be9f7c0 100644 --- a/Application.php +++ b/Application.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Console; -use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\CompleteCommand; use Symfony\Component\Console\Command\DumpCompletionCommand; @@ -29,7 +28,6 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; -use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\NamespaceNotFoundException; use Symfony\Component\Console\Exception\RuntimeException; diff --git a/Attribute/Argument.php b/Attribute/Argument.php index f2c813d3b..203dcc2af 100644 --- a/Attribute/Argument.php +++ b/Attribute/Argument.php @@ -116,7 +116,7 @@ public function resolveValue(InputInterface $input): mixed { $value = $input->getArgument($this->name); - if (is_subclass_of($this->typeName, \BackedEnum::class) && (is_string($value) || is_int($value))) { + if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) { return ($this->typeName)::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues); } diff --git a/Attribute/Option.php b/Attribute/Option.php index 8065d6ad8..6781e7dbd 100644 --- a/Attribute/Option.php +++ b/Attribute/Option.php @@ -146,7 +146,7 @@ public function resolveValue(InputInterface $input): mixed return true; } - if (is_subclass_of($this->typeName, \BackedEnum::class) && (is_string($value) || is_int($value))) { + if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) { return ($this->typeName)::tryFrom($value) ?? throw InvalidOptionException::fromEnumValue($this->name, $value, $this->suggestedValues); } @@ -168,7 +168,7 @@ public function resolveValue(InputInterface $input): mixed private function handleUnion(\ReflectionUnionType $type): self { $types = array_map( - static fn(\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null, + static fn (\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null, $type->getTypes(), ); diff --git a/Command/Command.php b/Command/Command.php index 9e6e41ec9..1d2e12bdc 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -91,9 +91,9 @@ public function __construct(?string $name = null, ?callable $code = null) { $this->definition = new InputDefinition(); - if ($code !== null) { + if (null !== $code) { if (!\is_object($code) || $code instanceof \Closure) { - throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)); + throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', self::class)); } /** @var AsCommand $attribute */ @@ -159,7 +159,7 @@ public function __construct(?string $name = null, ?callable $code = null) $this->addUsage($usage); } - if (\is_callable($this) && (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name === self::class) { + if (\is_callable($this) && self::class === (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name) { $this->code = new InvokableCommand($this, $this(...)); } diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php index 422f5601f..8bd0dceb4 100644 --- a/Tests/Command/InvokableCommandTest.php +++ b/Tests/Command/InvokableCommandTest.php @@ -240,6 +240,7 @@ public function testExecuteHasPriorityOverInvokeMethod() { $command = new class extends Command { public string $called; + protected function execute(InputInterface $input, OutputInterface $output): int { $this->called = __FUNCTION__; diff --git a/Tests/Helper/ProgressBarTest.php b/Tests/Helper/ProgressBarTest.php index ba74035f5..c0278cc33 100644 --- a/Tests/Helper/ProgressBarTest.php +++ b/Tests/Helper/ProgressBarTest.php @@ -423,7 +423,7 @@ public function testOverwriteWithSectionOutputAndEol() $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); $bar = new ProgressBar($output, 50, 0); - $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%' . PHP_EOL); + $bar->setFormat('[%bar%] %percent:3s%%'.\PHP_EOL.'%message%'.\PHP_EOL); $bar->setMessage(''); $bar->start(); $bar->display(); @@ -435,8 +435,8 @@ public function testOverwriteWithSectionOutputAndEol() rewind($output->getStream()); $this->assertEquals(escapeshellcmd( '[>---------------------------] 0%'.\PHP_EOL.\PHP_EOL. - "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL. 'Doing something...' . \PHP_EOL . - "\x1b[2A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. 'Doing something foo...' . \PHP_EOL), + "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL.'Doing something...'.\PHP_EOL. + "\x1b[2A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL.'Doing something foo...'.\PHP_EOL), escapeshellcmd(stream_get_contents($output->getStream())) ); } @@ -448,7 +448,7 @@ public function testOverwriteWithSectionOutputAndEolWithEmptyMessage() $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); $bar = new ProgressBar($output, 50, 0); - $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%'); + $bar->setFormat('[%bar%] %percent:3s%%'.\PHP_EOL.'%message%'); $bar->setMessage('Start'); $bar->start(); $bar->display(); @@ -460,8 +460,8 @@ public function testOverwriteWithSectionOutputAndEolWithEmptyMessage() rewind($output->getStream()); $this->assertEquals(escapeshellcmd( '[>---------------------------] 0%'.\PHP_EOL.'Start'.\PHP_EOL. - "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL . - "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. 'Doing something...' . \PHP_EOL), + "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL. + "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL.'Doing something...'.\PHP_EOL), escapeshellcmd(stream_get_contents($output->getStream())) ); } @@ -473,7 +473,7 @@ public function testOverwriteWithSectionOutputAndEolWithEmptyMessageComment() $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); $bar = new ProgressBar($output, 50, 0); - $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%'); + $bar->setFormat('[%bar%] %percent:3s%%'.\PHP_EOL.'%message%'); $bar->setMessage('Start'); $bar->start(); $bar->display(); @@ -485,8 +485,8 @@ public function testOverwriteWithSectionOutputAndEolWithEmptyMessageComment() rewind($output->getStream()); $this->assertEquals(escapeshellcmd( '[>---------------------------] 0%'.\PHP_EOL."\x1b[33mStart\x1b[39m".\PHP_EOL. - "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL . - "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. "\x1b[33mDoing something...\x1b[39m" . \PHP_EOL), + "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL. + "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL."\x1b[33mDoing something...\x1b[39m".\PHP_EOL), escapeshellcmd(stream_get_contents($output->getStream())) ); } diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 52ae23301..eb85364da 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -2099,12 +2099,12 @@ public function testGithubIssue60038WidthOfCellWithEmoji() ->setHeaderTitle('Test Title') ->setHeaders(['Title', 'Author']) ->setRows([ - ["🎭 💫 ☯"." Divine Comedy", "Dante Alighieri"], + ['🎭 💫 ☯ Divine Comedy', 'Dante Alighieri'], // the snowflake (e2 9d 84 ef b8 8f) has a variant selector - ["👑 ❄️ 🗡"." Game of Thrones", "George R.R. Martin"], + ['👑 ❄️ 🗡 Game of Thrones', 'George R.R. Martin'], // the snowflake in text style (e2 9d 84 ef b8 8e) has a variant selector - ["❄︎❄︎❄︎ snowflake in text style ❄︎❄︎❄︎", ""], - ["And a very long line to show difference in previous lines", ""], + ['❄︎❄︎❄︎ snowflake in text style ❄︎❄︎❄︎', ''], + ['And a very long line to show difference in previous lines', ''], ]) ; $table->render(); From 03268a90326248196c78ae447751abaa781e0733 Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Sat, 12 Jul 2025 15:55:19 +0200 Subject: [PATCH 16/36] optimize `in_array` calls --- Helper/QuestionHelper.php | 2 +- Tests/Helper/QuestionHelperTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 9b65c3213..fcbc56fda 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -577,7 +577,7 @@ private function cloneInputStream($inputStream) // For seekable and writable streams, add all the same data to the // cloned stream and then seek to the same offset. - if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) { + if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'], true)) { $offset = ftell($inputStream); rewind($inputStream); stream_copy_to_stream($inputStream, $cloneStream); diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index 0e91dd85b..b6ecc5ed3 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -565,7 +565,7 @@ public function testAskAndValidate() $error = 'This is not a color!'; $validator = function ($color) use ($error) { - if (!\in_array($color, ['white', 'black'])) { + if (!\in_array($color, ['white', 'black'], true)) { throw new \InvalidArgumentException($error); } From a7930e07345718263a83133a0e03e468b242f0d0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 18 Jul 2025 10:19:51 +0200 Subject: [PATCH 17/36] [Console] Fix merge --- Tests/ApplicationTest.php | 4 ++-- Tests/Command/TraceableCommandTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index e9b45c051..5e6f47330 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -223,12 +223,12 @@ public function testRegisterAmbiguous() $this->assertStringContainsString('It works!', $tester->getDisplay(true)); } - public function testAdd() + public function testAddCommand() { $application = new Application(); $application->addCommand($foo = new \FooCommand()); $commands = $application->all(); - $this->assertEquals($foo, $commands['foo:bar'], '->add() registers a command'); + $this->assertEquals($foo, $commands['foo:bar'], '->addCommand() registers a command'); $application = new Application(); $application->addCommands([$foo = new \FooCommand(), $foo1 = new \Foo1Command()]); diff --git a/Tests/Command/TraceableCommandTest.php b/Tests/Command/TraceableCommandTest.php index 2775ec7e9..c7e897484 100644 --- a/Tests/Command/TraceableCommandTest.php +++ b/Tests/Command/TraceableCommandTest.php @@ -25,7 +25,7 @@ class TraceableCommandTest extends TestCase protected function setUp(): void { $this->application = new Application(); - $this->application->add(new LoopExampleCommand()); + $this->application->addCommand(new LoopExampleCommand()); } public function testRunIsOverriddenWithoutProfile() @@ -47,7 +47,7 @@ public function testRunIsNotOverriddenWithProfile() $command = new LoopExampleCommand(); $traceableCommand = new TraceableCommand($command, new Stopwatch()); - $this->application->add($traceableCommand); + $this->application->addCommand($traceableCommand); $commandTester = new CommandTester($traceableCommand); $commandTester->execute([]); From 9d8d688fb6d13c13bb78587ee5c3e01d10517993 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 22 Jul 2025 14:41:25 +0200 Subject: [PATCH 18/36] [Console] cleanup --- Input/InputOption.php | 2 +- Tests/Input/InputOptionTest.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Input/InputOption.php b/Input/InputOption.php index 25fb91782..4f8e66e5c 100644 --- a/Input/InputOption.php +++ b/Input/InputOption.php @@ -79,7 +79,7 @@ public function __construct( throw new InvalidArgumentException('An option name cannot be empty.'); } - if ('' === $shortcut || [] === $shortcut || false === $shortcut) { + if ('' === $shortcut || [] === $shortcut) { $shortcut = null; } diff --git a/Tests/Input/InputOptionTest.php b/Tests/Input/InputOptionTest.php index 47ab503f7..e5cb3318a 100644 --- a/Tests/Input/InputOptionTest.php +++ b/Tests/Input/InputOptionTest.php @@ -73,8 +73,6 @@ public function testShortcut() $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in an array'); $option = new InputOption('foo', '0|z'); $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in a string-list'); - $option = new InputOption('foo', false); - $this->assertNull($option->getShortcut(), '__construct() makes the shortcut null when given a false as value'); } public function testModes() From 72da4318618da31f93f814a994230108b90ccfe1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 24 Jul 2025 14:45:41 +0200 Subject: [PATCH 19/36] Fix typos --- Tests/ApplicationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 5e6f47330..f7ac71ef5 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -889,7 +889,7 @@ public function testSetCatchErrors(bool $catchExceptions) try { $tester->run(['command' => 'boom']); - $this->fail('The exception is not catched.'); + $this->fail('The exception is not caught.'); } catch (\Throwable $e) { $this->assertInstanceOf(\Error::class, $e); $this->assertSame('This is an error.', $e->getMessage()); From efb76ce5b1a1b9ec7c88f3f02c5c33cc6d4c776a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 9 Oct 2024 11:06:51 +0200 Subject: [PATCH 20/36] run tests using PHPUnit 11.5 --- Tests/Command/CommandTest.php | 25 ++++++++++--------------- phpunit.xml.dist | 21 +++++++++------------ 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index a4a719b3d..87c7b0790 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -11,8 +11,9 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -31,8 +32,6 @@ class CommandTest extends TestCase { - use ExpectUserDeprecationMessageTrait; - protected static string $fixturesPath; public static function setUpBeforeClass(): void @@ -471,9 +470,8 @@ public function testCommandAttribute() $this->assertSame(['f'], $command->getAliases()); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testCommandAttributeWithDeprecatedMethods() { $this->expectUserDeprecationMessage('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultName()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); @@ -491,9 +489,8 @@ public function testAttributeOverridesProperty() $this->assertSame('This is a command I wrote all by myself', $command->getDescription()); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testAttributeOverridesPropertyWithDeprecatedMethods() { $this->expectUserDeprecationMessage('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultName()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); @@ -517,9 +514,8 @@ public function testDefaultCommand() $this->assertEquals('foo2', $property->getValue($apl)); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testDeprecatedMethods() { $this->expectUserDeprecationMessage('Since symfony/console 7.3: Overriding "Command::getDefaultName()" in "Symfony\Component\Console\Tests\Command\FooCommand" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); @@ -528,9 +524,8 @@ public function testDeprecatedMethods() new FooCommand(); } - /** - * @group legacy - */ + #[IgnoreDeprecations] + #[Group('legacy')] public function testDeprecatedNonIntegerReturnTypeFromClosureCode() { $this->expectUserDeprecationMessage('Since symfony/console 7.3: Returning a non-integer value from the command "foo" is deprecated and will throw an exception in Symfony 8.0.'); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0e96921be..950f1c096 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,11 @@ @@ -18,7 +19,7 @@ - + ./ @@ -27,15 +28,11 @@ ./Tests ./vendor - + - - - - - Symfony\Component\Console - - - - + + + + + From d3709f93857d2ec9e06f910b374af12fc4f5cf25 Mon Sep 17 00:00:00 2001 From: mamazu <14860264+mamazu@users.noreply.github.com> Date: Sat, 2 Aug 2025 22:55:02 +0200 Subject: [PATCH 21/36] Adding more helpful error messages to the Questionhelper --- Helper/QuestionHelper.php | 2 +- Tests/Helper/QuestionHelperTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index fcbc56fda..da7db4f1f 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -271,7 +271,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { shell_exec('stty '.$sttyMode); - throw new MissingInputException('Aborted.'); + throw new MissingInputException('Aborted while asking: '.$question->getQuestion()); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { --$i; diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index b6ecc5ed3..555cbe724 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -750,7 +750,7 @@ public function testAskThrowsExceptionOnMissingInput() public function testAskThrowsExceptionOnMissingInputForChoiceQuestion() { $this->expectException(MissingInputException::class); - $this->expectExceptionMessage('Aborted.'); + $this->expectExceptionMessage('Aborted while asking: Choice'); (new QuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b'])); } From e6bee101225841c01ee9f214cf32c8483f9fb7ca Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 31 Jul 2025 14:36:46 +0200 Subject: [PATCH 22/36] replace PHPUnit annotations with attributes --- Tests/ApplicationTest.php | 119 ++++++------------ Tests/CI/GithubActionReporterTest.php | 5 +- Tests/Command/CommandTest.php | 16 +-- Tests/Command/CompleteCommandTest.php | 13 +- Tests/Command/DumpCompletionCommandTest.php | 5 +- Tests/Command/HelpCommandTest.php | 5 +- Tests/Command/InvokableCommandTest.php | 17 +-- Tests/Command/ListCommandTest.php | 5 +- Tests/Completion/CompletionInputTest.php | 13 +- .../AddConsoleCommandPassTest.php | 5 +- .../Descriptor/AbstractDescriptorTestCase.php | 11 +- .../Descriptor/ApplicationDescriptionTest.php | 5 +- Tests/Formatter/OutputFormatterTest.php | 15 +-- Tests/Helper/DumperNativeFallbackTest.php | 5 +- Tests/Helper/DumperTest.php | 5 +- Tests/Helper/HelperTest.php | 9 +- Tests/Helper/OutputWrapperTest.php | 5 +- Tests/Helper/ProcessHelperTest.php | 5 +- Tests/Helper/ProgressBarTest.php | 10 +- Tests/Helper/ProgressIndicatorTest.php | 10 +- Tests/Helper/QuestionHelperTest.php | 26 ++-- Tests/Helper/SymfonyQuestionHelperTest.php | 5 +- Tests/Helper/TableTest.php | 25 ++-- Tests/Input/ArgvInputTest.php | 25 ++-- Tests/Input/ArrayInputTest.php | 9 +- Tests/Input/InputDefinitionTest.php | 5 +- Tests/Input/StringInputTest.php | 5 +- Tests/Logger/ConsoleLoggerTest.php | 9 +- Tests/Output/AnsiColorModeTest.php | 9 +- Tests/Output/OutputTest.php | 9 +- Tests/Question/ChoiceQuestionTest.php | 9 +- Tests/Question/ConfirmationQuestionTest.php | 5 +- Tests/Question/QuestionTest.php | 33 ++--- Tests/SignalRegistry/SignalMapTest.php | 5 +- Tests/SignalRegistry/SignalRegistryTest.php | 5 +- Tests/Style/SymfonyStyleTest.php | 9 +- Tests/TerminalTest.php | 5 +- .../Constraint/CommandIsSuccessfulTest.php | 5 +- 38 files changed, 161 insertions(+), 325 deletions(-) diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index f7ac71ef5..684ebeb91 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Console\Tests; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; @@ -272,9 +276,7 @@ public function testAddCommandWithInvokableExtendedCommand() $this->assertEquals($foo, $commands['invokable:test']); } - /** - * @dataProvider provideInvalidInvokableCommands - */ + #[DataProvider('provideInvalidInvokableCommands')] public function testAddCommandThrowsExceptionOnInvalidCommand(callable $command, string $expectedException, string $expectedExceptionMessage) { $application = new Application(); @@ -492,9 +494,7 @@ public function testFindWithCommandLoader() $this->assertInstanceOf(\FooCommand::class, $application->find('a'), '->find() returns a command if the abbreviation exists for an alias'); } - /** - * @dataProvider provideAmbiguousAbbreviations - */ + #[DataProvider('provideAmbiguousAbbreviations')] public function testFindWithAmbiguousAbbreviations($abbreviation, $expectedExceptionMessage) { putenv('COLUMNS=120'); @@ -567,9 +567,7 @@ public function testFindCommandWithMissingNamespace() $this->assertInstanceOf(\Foo4Command::class, $application->find('f::t')); } - /** - * @dataProvider provideInvalidCommandNamesSingle - */ + #[DataProvider('provideInvalidCommandNamesSingle')] public function testFindAlternativeExceptionMessageSingle($name) { $application = new Application(); @@ -841,10 +839,8 @@ public function testFindAmbiguousCommandsIfAllAlternativesAreHidden() $this->assertInstanceOf(\FooCommand::class, $application->find('foo:')); } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testSetCatchExceptions(bool $catchErrors) { $application = new Application(); @@ -873,10 +869,8 @@ public function testSetCatchExceptions(bool $catchErrors) } } - /** - * @testWith [true] - * [false] - */ + #[TestWith([true])] + #[TestWith([false])] public function testSetCatchErrors(bool $catchExceptions) { $application = new Application(); @@ -1015,9 +1009,7 @@ public function testRenderExceptionLineBreaks() $this->assertStringMatchesFormatFile(self::$fixturesPath.'/application_renderexception_linebreaks.txt', $tester->getDisplay(true), '->renderException() keep multiple line breaks'); } - /** - * @group transient-on-windows - */ + #[Group('transient-on-windows')] public function testRenderAnonymousException() { $application = new Application(); @@ -1041,9 +1033,7 @@ public function testRenderAnonymousException() $this->assertStringContainsString('Dummy type "class@anonymous" is invalid.', $tester->getDisplay(true)); } - /** - * @group transient-on-windows - */ + #[Group('transient-on-windows')] public function testRenderExceptionStackTraceContainsRootException() { $application = new Application(); @@ -1302,10 +1292,8 @@ public function testRunDispatchesExitCodeOneForExceptionCodeZero() $this->assertTrue($passedRightValue, '-> exit code 1 was passed in the console.terminate event'); } - /** - * @testWith [-1] - * [-32000] - */ + #[TestWith([-1])] + #[TestWith([-32000])] public function testRunReturnsExitCodeOneForNegativeExceptionCode($exceptionCode) { $exception = new \Exception('', $exceptionCode); @@ -1349,9 +1337,7 @@ public function testAddingOptionWithDuplicateShortcut() $application->run($input, $output); } - /** - * @dataProvider getAddingAlreadySetDefinitionElementData - */ + #[DataProvider('getAddingAlreadySetDefinitionElementData')] public function testAddingAlreadySetDefinitionElementData($def) { $application = new Application(); @@ -2039,9 +2025,7 @@ public function testCommandNameMismatchWithCommandLoaderKeyThrows() $app->get('test'); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalListenerNotCalledByDefault() { $command = new SignableCommand(false); @@ -2059,9 +2043,7 @@ public function testSignalListenerNotCalledByDefault() $this->assertFalse($dispatcherCalled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalListener() { $command = new SignableCommand(); @@ -2080,9 +2062,7 @@ public function testSignalListener() $this->assertTrue($command->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalSubscriberNotCalledByDefault() { $command = new BaseSignableCommand(false); @@ -2097,9 +2077,7 @@ public function testSignalSubscriberNotCalledByDefault() $this->assertFalse($subscriber->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalSubscriber() { $command = new BaseSignableCommand(); @@ -2118,9 +2096,7 @@ public function testSignalSubscriber() $this->assertTrue($subscriber2->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalDispatchWithoutEventToDispatch() { $command = new SignableCommand(); @@ -2132,9 +2108,7 @@ public function testSignalDispatchWithoutEventToDispatch() $this->assertTrue($command->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalDispatchWithoutEventDispatcher() { $command = new SignableCommand(); @@ -2146,9 +2120,7 @@ public function testSignalDispatchWithoutEventDispatcher() $this->assertTrue($command->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSetSignalsToDispatchEvent() { if (!\defined('SIGUSR1')) { @@ -2280,9 +2252,7 @@ public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals() $this->assertTrue($terminateEventDispatched); } - /** - * @group tty - */ + #[Group('tty')] public function testSignalableRestoresStty() { if (!Terminal::hasSttyAvailable()) { @@ -2313,9 +2283,7 @@ public function testSignalableRestoresStty() $this->assertSame($previousSttyMode, $sttyMode); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalableInvokableCommand() { $command = new Command(); @@ -2331,9 +2299,7 @@ public function testSignalableInvokableCommand() $this->assertTrue($invokable->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalableInvokableCommandThatExtendsBaseCommand() { $command = new class extends Command implements SignalableCommandInterface { @@ -2348,9 +2314,7 @@ public function testSignalableInvokableCommandThatExtendsBaseCommand() $this->assertTrue($command->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testAlarmSubscriberNotCalledByDefault() { $command = new BaseSignableCommand(false); @@ -2366,9 +2330,7 @@ public function testAlarmSubscriberNotCalledByDefault() $this->assertFalse($subscriber->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testAlarmSubscriberNotCalledForOtherSignals() { $command = new SignableCommand(); @@ -2387,9 +2349,7 @@ public function testAlarmSubscriberNotCalledForOtherSignals() $this->assertFalse($subscriber2->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testAlarmSubscriber() { $command = new BaseSignableCommand(signal: \SIGALRM); @@ -2408,9 +2368,7 @@ public function testAlarmSubscriber() $this->assertTrue($subscriber2->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testAlarmDispatchWithoutEventDispatcher() { $command = new AlarmableCommand(1); @@ -2423,9 +2381,7 @@ public function testAlarmDispatchWithoutEventDispatcher() $this->assertTrue($command->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testAlarmableCommandWithoutInterval() { $command = new AlarmableCommand(0); @@ -2442,9 +2398,7 @@ public function testAlarmableCommandWithoutInterval() $this->assertFalse($command->signaled); } - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testAlarmableCommandHandlerCalledAfterEventListener() { $command = new AlarmableCommand(1); @@ -2461,12 +2415,9 @@ public function testAlarmableCommandHandlerCalledAfterEventListener() $this->assertSame([AlarmEventSubscriber::class, AlarmableCommand::class], $command->signalHandlers); } - /** - * @requires extension pcntl - * - * @testWith [false] - * [4] - */ + #[RequiresPhpExtension('pcntl')] + #[TestWith([false])] + #[TestWith([4])] public function testAlarmSubscriberCalledAfterSignalSubscriberAndInheritsExitCode(int|false $exitCode) { $command = new BaseSignableCommand(signal: \SIGALRM); diff --git a/Tests/CI/GithubActionReporterTest.php b/Tests/CI/GithubActionReporterTest.php index a35927950..f4ee70eaf 100644 --- a/Tests/CI/GithubActionReporterTest.php +++ b/Tests/CI/GithubActionReporterTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\CI; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Output\BufferedOutput; @@ -31,9 +32,7 @@ public function testIsGithubActionEnvironment() } } - /** - * @dataProvider annotationsFormatProvider - */ + #[DataProvider('annotationsFormatProvider')] public function testAnnotationsFormat(string $type, string $message, ?string $file, ?int $line, ?int $col, string $expected) { $reporter = new GithubActionReporter($buffer = new BufferedOutput()); diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index 87c7b0790..f07b76a36 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -11,8 +11,10 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; @@ -137,9 +139,7 @@ public function testGetNamespaceGetNameSetName() $this->assertEquals('foobar:bar', $command->getName(), '->setName() sets the command name'); } - /** - * @dataProvider provideInvalidCommandNames - */ + #[DataProvider('provideInvalidCommandNames')] public function testInvalidCommandNames($name) { $this->expectException(\InvalidArgumentException::class); @@ -205,10 +205,8 @@ public function testGetSetAliases() $this->assertEquals(['name1'], $command->getAliases(), '->setAliases() sets the aliases'); } - /** - * @testWith ["name|alias1|alias2", "name", ["alias1", "alias2"], false] - * ["|alias1|alias2", "alias1", ["alias2"], true] - */ + #[TestWith(['name|alias1|alias2', 'name', ['alias1', 'alias2'], false])] + #[TestWith(['|alias1|alias2', 'alias1', ['alias2'], true])] public function testSetAliasesAndHiddenViaName(string $name, string $expectedName, array $expectedAliases, bool $expectedHidden) { $command = new Command($name); @@ -390,9 +388,7 @@ public static function getSetCodeBindToClosureTests() ]; } - /** - * @dataProvider getSetCodeBindToClosureTests - */ + #[DataProvider('getSetCodeBindToClosureTests')] public function testSetCodeBindToClosure($previouslyBound, $expected) { $code = createClosure(); diff --git a/Tests/Command/CompleteCommandTest.php b/Tests/Command/CompleteCommandTest.php index 08f6b046f..8f26b0360 100644 --- a/Tests/Command/CompleteCommandTest.php +++ b/Tests/Command/CompleteCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -65,9 +66,7 @@ public function testAdditionalShellSupport() $this->execute(['--shell' => 'bash', '--current' => '1', '--input' => ['bin/console']]); } - /** - * @dataProvider provideInputAndCurrentOptionValues - */ + #[DataProvider('provideInputAndCurrentOptionValues')] public function testInputAndCurrentOptionValidation(array $input, ?string $exceptionMessage) { if ($exceptionMessage) { @@ -91,9 +90,7 @@ public static function provideInputAndCurrentOptionValues() yield [['--current' => '2', '--input' => ['bin/console', 'cache:clear']], null]; } - /** - * @dataProvider provideCompleteCommandNameInputs - */ + #[DataProvider('provideCompleteCommandNameInputs')] public function testCompleteCommandName(array $input, array $suggestions) { $this->execute(['--current' => '1', '--input' => $input]); @@ -108,9 +105,7 @@ public static function provideCompleteCommandNameInputs() yield 'complete-aliases' => [['bin/console', 'ah'], ['hello', 'ahoy']]; } - /** - * @dataProvider provideCompleteCommandInputDefinitionInputs - */ + #[DataProvider('provideCompleteCommandInputDefinitionInputs')] public function testCompleteCommandInputDefinition(array $input, array $suggestions) { $this->execute(['--current' => '2', '--input' => $input]); diff --git a/Tests/Command/DumpCompletionCommandTest.php b/Tests/Command/DumpCompletionCommandTest.php index ba23bb331..a69711e0e 100644 --- a/Tests/Command/DumpCompletionCommandTest.php +++ b/Tests/Command/DumpCompletionCommandTest.php @@ -11,15 +11,14 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\DumpCompletionCommand; use Symfony\Component\Console\Tester\CommandCompletionTester; class DumpCompletionCommandTest extends TestCase { - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $tester = new CommandCompletionTester(new DumpCompletionCommand()); diff --git a/Tests/Command/HelpCommandTest.php b/Tests/Command/HelpCommandTest.php index f1979c0dc..160d78560 100644 --- a/Tests/Command/HelpCommandTest.php +++ b/Tests/Command/HelpCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\HelpCommand; @@ -70,9 +71,7 @@ public function testExecuteForApplicationCommandWithXmlOption() $this->assertStringContainsString('getDisplay(), '->execute() returns an XML help text if --format=xml is passed'); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { require_once realpath(__DIR__.'/../Fixtures/FooCommand.php'); diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php index 8bd0dceb4..1eac29944 100644 --- a/Tests/Command/InvokableCommandTest.php +++ b/Tests/Command/InvokableCommandTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Tests\Command; use PHPUnit\Framework\Assert; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\Option; @@ -292,9 +293,7 @@ public function __invoke() $command->run(new ArrayInput([]), new NullOutput()); } - /** - * @dataProvider provideInputArguments - */ + #[DataProvider('provideInputArguments')] public function testInputArguments(array $parameters, array $expected) { $command = new Command('foo'); @@ -322,9 +321,7 @@ public static function provideInputArguments(): \Generator yield 'required & without-value' => [['a' => 'x', 'b' => null, 'c' => null, 'd' => null], ['x', null, '', []]]; } - /** - * @dataProvider provideBinaryInputOptions - */ + #[DataProvider('provideBinaryInputOptions')] public function testBinaryInputOptions(array $parameters, array $expected) { $command = new Command('foo'); @@ -350,9 +347,7 @@ public static function provideBinaryInputOptions(): \Generator yield 'negative' => [['--no-a' => null, '--no-c' => null], [false, false, false]]; } - /** - * @dataProvider provideNonBinaryInputOptions - */ + #[DataProvider('provideNonBinaryInputOptions')] public function testNonBinaryInputOptions(array $parameters, array $expected) { $command = new Command('foo'); @@ -405,9 +400,7 @@ public static function provideNonBinaryInputOptions(): \Generator ]; } - /** - * @dataProvider provideInvalidOptionDefinitions - */ + #[DataProvider('provideInvalidOptionDefinitions')] public function testInvalidOptionDefinition(callable $code) { $command = new Command('foo'); diff --git a/Tests/Command/ListCommandTest.php b/Tests/Command/ListCommandTest.php index 37496c6b3..7efdb5ab5 100644 --- a/Tests/Command/ListCommandTest.php +++ b/Tests/Command/ListCommandTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Command; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandCompletionTester; @@ -115,9 +116,7 @@ public function testExecuteListsCommandsOrderRaw() $this->assertEquals($output, trim($commandTester->getDisplay(true))); } - /** - * @dataProvider provideCompletionSuggestions - */ + #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { require_once realpath(__DIR__.'/../Fixtures/FooCommand.php'); diff --git a/Tests/Completion/CompletionInputTest.php b/Tests/Completion/CompletionInputTest.php index df0d081fd..58493fdf0 100644 --- a/Tests/Completion/CompletionInputTest.php +++ b/Tests/Completion/CompletionInputTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Completion; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Input\InputArgument; @@ -19,9 +20,7 @@ class CompletionInputTest extends TestCase { - /** - * @dataProvider provideBindData - */ + #[DataProvider('provideBindData')] public function testBind(CompletionInput $input, string $expectedType, ?string $expectedName, string $expectedValue) { $definition = new InputDefinition([ @@ -74,9 +73,7 @@ public static function provideBindData() yield 'end' => [CompletionInput::fromTokens(['bin/console', 'symfony', 'sensiolabs'], 3), CompletionInput::TYPE_NONE, null, '']; } - /** - * @dataProvider provideBindWithLastArrayArgumentData - */ + #[DataProvider('provideBindWithLastArrayArgumentData')] public function testBindWithLastArrayArgument(CompletionInput $input, ?string $expectedValue) { $definition = new InputDefinition([ @@ -111,9 +108,7 @@ public function testBindArgumentWithDefault() $this->assertEquals('', $input->getCompletionValue(), 'Unexpected value'); } - /** - * @dataProvider provideFromStringData - */ + #[DataProvider('provideFromStringData')] public function testFromString($inputStr, array $expectedTokens) { $input = CompletionInput::fromString($inputStr, 1); diff --git a/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/Tests/DependencyInjection/AddConsoleCommandPassTest.php index a11e6b510..953e5843c 100644 --- a/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\DependencyInjection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -28,9 +29,7 @@ class AddConsoleCommandPassTest extends TestCase { - /** - * @dataProvider visibilityProvider - */ + #[DataProvider('visibilityProvider')] public function testProcess($public) { $container = new ContainerBuilder(); diff --git a/Tests/Descriptor/AbstractDescriptorTestCase.php b/Tests/Descriptor/AbstractDescriptorTestCase.php index 93658f4be..0c4eee158 100644 --- a/Tests/Descriptor/AbstractDescriptorTestCase.php +++ b/Tests/Descriptor/AbstractDescriptorTestCase.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Descriptor; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -21,31 +22,31 @@ abstract class AbstractDescriptorTestCase extends TestCase { - /** @dataProvider getDescribeInputArgumentTestData */ + #[DataProvider('getDescribeInputArgumentTestData')] public function testDescribeInputArgument(InputArgument $argument, $expectedDescription) { $this->assertDescription($expectedDescription, $argument); } - /** @dataProvider getDescribeInputOptionTestData */ + #[DataProvider('getDescribeInputOptionTestData')] public function testDescribeInputOption(InputOption $option, $expectedDescription) { $this->assertDescription($expectedDescription, $option); } - /** @dataProvider getDescribeInputDefinitionTestData */ + #[DataProvider('getDescribeInputDefinitionTestData')] public function testDescribeInputDefinition(InputDefinition $definition, $expectedDescription) { $this->assertDescription($expectedDescription, $definition); } - /** @dataProvider getDescribeCommandTestData */ + #[DataProvider('getDescribeCommandTestData')] public function testDescribeCommand(Command $command, $expectedDescription) { $this->assertDescription($expectedDescription, $command); } - /** @dataProvider getDescribeApplicationTestData */ + #[DataProvider('getDescribeApplicationTestData')] public function testDescribeApplication(Application $application, $expectedDescription) { // the "completion" command has dynamic help information depending on the shell diff --git a/Tests/Descriptor/ApplicationDescriptionTest.php b/Tests/Descriptor/ApplicationDescriptionTest.php index ab90320cd..a6117952a 100644 --- a/Tests/Descriptor/ApplicationDescriptionTest.php +++ b/Tests/Descriptor/ApplicationDescriptionTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Descriptor; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; @@ -18,9 +19,7 @@ final class ApplicationDescriptionTest extends TestCase { - /** - * @dataProvider getNamespacesProvider - */ + #[DataProvider('getNamespacesProvider')] public function testGetNamespaces(array $expected, array $names) { $application = new TestApplication(); diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php index b66b6abe4..2ffe7b301 100644 --- a/Tests/Formatter/OutputFormatterTest.php +++ b/Tests/Formatter/OutputFormatterTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Formatter; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; @@ -159,9 +160,7 @@ public function testInlineStyle() $this->assertEquals("\033[34;41msome text\033[39;49m", $formatter->format('some text')); } - /** - * @dataProvider provideInlineStyleOptionsCases - */ + #[DataProvider('provideInlineStyleOptionsCases')] public function testInlineStyleOptions(string $tag, ?string $expected = null, ?string $input = null, bool $truecolor = false) { if ($truecolor && 'truecolor' !== getenv('COLORTERM')) { @@ -177,7 +176,7 @@ public function testInlineStyleOptions(string $tag, ?string $expected = null, ?s $expected = $tag.$input.''; $this->assertSame($expected, $formatter->format($expected)); } else { - /** @var OutputFormatterStyle $result */ + /* @var OutputFormatterStyle $result */ $this->assertInstanceOf(OutputFormatterStyle::class, $result); $this->assertSame($expected, $formatter->format($tag.$input.'')); $this->assertSame($expected, $formatter->format($tag.$input.'')); @@ -241,9 +240,7 @@ public function testFormatterHasStyles() $this->assertTrue($formatter->hasStyle('question')); } - /** - * @dataProvider provideDecoratedAndNonDecoratedOutput - */ + #[DataProvider('provideDecoratedAndNonDecoratedOutput')] public function testNotDecoratedFormatterOnJediTermEmulator(string $input, string $expectedNonDecoratedOutput, string $expectedDecoratedOutput, bool $shouldBeJediTerm = false) { $terminalEmulator = $shouldBeJediTerm ? 'JetBrains-JediTerm' : 'Unknown'; @@ -259,9 +256,7 @@ public function testNotDecoratedFormatterOnJediTermEmulator(string $input, strin } } - /** - * @dataProvider provideDecoratedAndNonDecoratedOutput - */ + #[DataProvider('provideDecoratedAndNonDecoratedOutput')] public function testNotDecoratedFormatterOnIDEALikeEnvironment(string $input, string $expectedNonDecoratedOutput, string $expectedDecoratedOutput, bool $expectsIDEALikeTerminal = false) { // Backup previous env variable diff --git a/Tests/Helper/DumperNativeFallbackTest.php b/Tests/Helper/DumperNativeFallbackTest.php index 1b37e4e93..892590dd7 100644 --- a/Tests/Helper/DumperNativeFallbackTest.php +++ b/Tests/Helper/DumperNativeFallbackTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ClassExistsMock; use Symfony\Component\Console\Helper\Dumper; @@ -32,9 +33,7 @@ public static function tearDownAfterClass(): void ClassExistsMock::withMockedClasses([]); } - /** - * @dataProvider provideVariables - */ + #[DataProvider('provideVariables')] public function testInvoke($variable, $primitiveString) { $dumper = new Dumper($this->createMock(OutputInterface::class)); diff --git a/Tests/Helper/DumperTest.php b/Tests/Helper/DumperTest.php index 0a30c6ec3..344d3f4dc 100644 --- a/Tests/Helper/DumperTest.php +++ b/Tests/Helper/DumperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Helper\Dumper; use Symfony\Component\Console\Output\OutputInterface; @@ -32,9 +33,7 @@ public static function tearDownAfterClass(): void putenv('DUMP_COMMA_SEPARATOR'); } - /** - * @dataProvider provideVariables - */ + #[DataProvider('provideVariables')] public function testInvoke($variable) { $output = $this->createMock(OutputInterface::class); diff --git a/Tests/Helper/HelperTest.php b/Tests/Helper/HelperTest.php index 009864454..95d5ee5d3 100644 --- a/Tests/Helper/HelperTest.php +++ b/Tests/Helper/HelperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; @@ -61,17 +62,13 @@ public static function decoratedTextProvider() ]; } - /** - * @dataProvider formatTimeProvider - */ + #[DataProvider('formatTimeProvider')] public function testFormatTime(int|float $secs, string $expectedFormat, int $precision) { $this->assertEquals($expectedFormat, Helper::formatTime($secs, $precision)); } - /** - * @dataProvider decoratedTextProvider - */ + #[DataProvider('decoratedTextProvider')] public function testRemoveDecoration(string $decoratedText, string $undecoratedText) { $this->assertEquals($undecoratedText, Helper::removeDecoration(new OutputFormatter(), $decoratedText)); diff --git a/Tests/Helper/OutputWrapperTest.php b/Tests/Helper/OutputWrapperTest.php index 2ce15b6d4..d09fd9ea9 100644 --- a/Tests/Helper/OutputWrapperTest.php +++ b/Tests/Helper/OutputWrapperTest.php @@ -11,14 +11,13 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Helper\OutputWrapper; class OutputWrapperTest extends TestCase { - /** - * @dataProvider textProvider - */ + #[DataProvider('textProvider')] public function testBasicWrap(string $text, int $width, bool $allowCutUrls, string $expected) { $wrapper = new OutputWrapper($allowCutUrls); diff --git a/Tests/Helper/ProcessHelperTest.php b/Tests/Helper/ProcessHelperTest.php index 1fd88987b..02d9fb939 100644 --- a/Tests/Helper/ProcessHelperTest.php +++ b/Tests/Helper/ProcessHelperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\HelperSet; @@ -20,9 +21,7 @@ class ProcessHelperTest extends TestCase { - /** - * @dataProvider provideCommandsAndOutput - */ + #[DataProvider('provideCommandsAndOutput')] public function testVariousProcessRuns(string $expected, Process|string|array $cmd, int $verbosity, ?string $error) { if (\is_string($cmd)) { diff --git a/Tests/Helper/ProgressBarTest.php b/Tests/Helper/ProgressBarTest.php index c0278cc33..683bc7c87 100644 --- a/Tests/Helper/ProgressBarTest.php +++ b/Tests/Helper/ProgressBarTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; @@ -18,9 +20,7 @@ use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\StreamOutput; -/** - * @group time-sensitive - */ +#[Group('time-sensitive')] class ProgressBarTest extends TestCase { private string|false $colSize; @@ -1117,9 +1117,7 @@ public function testUnicode() $bar->finish(); } - /** - * @dataProvider provideFormat - */ + #[DataProvider('provideFormat')] public function testFormatsWithoutMax($format) { $bar = new ProgressBar($output = $this->getOutputStream(), 0, 0); diff --git a/Tests/Helper/ProgressIndicatorTest.php b/Tests/Helper/ProgressIndicatorTest.php index 2a4441d57..fb11d1432 100644 --- a/Tests/Helper/ProgressIndicatorTest.php +++ b/Tests/Helper/ProgressIndicatorTest.php @@ -11,13 +11,13 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Output\StreamOutput; -/** - * @group time-sensitive - */ +#[Group('time-sensitive')] class ProgressIndicatorTest extends TestCase { public function testDefaultIndicator() @@ -176,9 +176,7 @@ public function testCannotFinishUnstartedIndicator() $bar->finish('Finished'); } - /** - * @dataProvider provideFormat - */ + #[DataProvider('provideFormat')] public function testFormats($format) { $bar = new ProgressIndicator($output = $this->getOutputStream(), $format); diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index b6ecc5ed3..8ea4b1934 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\MissingInputException; @@ -27,9 +29,7 @@ use Symfony\Component\Console\Terminal; use Symfony\Component\Console\Tester\ApplicationTester; -/** - * @group tty - */ +#[Group('tty')] class QuestionHelperTest extends AbstractQuestionHelperTestCase { public function testAskChoice() @@ -348,9 +348,7 @@ public static function getInputs() ]; } - /** - * @dataProvider getInputs - */ + #[DataProvider('getInputs')] public function testAskWithAutocompleteWithMultiByteCharacter($character) { if (!Terminal::hasSttyAvailable()) { @@ -522,9 +520,7 @@ public function testAskMultilineResponseWithWithCursorInMiddleOfSeekableInputStr $this->assertSame(8, ftell($response)); } - /** - * @dataProvider getAskConfirmationData - */ + #[DataProvider('getAskConfirmationData')] public function testAskConfirmation($question, $expected, $default = true) { $dialog = new QuestionHelper(); @@ -588,9 +584,7 @@ public function testAskAndValidate() } } - /** - * @dataProvider simpleAnswerProvider - */ + #[DataProvider('simpleAnswerProvider')] public function testSelectChoiceFromSimpleChoices($providedAnswer, $expectedValue) { $possibleChoices = [ @@ -622,9 +616,7 @@ public static function simpleAnswerProvider() ]; } - /** - * @dataProvider specialCharacterInMultipleChoice - */ + #[DataProvider('specialCharacterInMultipleChoice')] public function testSpecialCharacterChoiceFromMultipleChoiceList($providedAnswer, $expectedValue) { $possibleChoices = [ @@ -653,9 +645,7 @@ public static function specialCharacterInMultipleChoice() ]; } - /** - * @dataProvider answerProvider - */ + #[DataProvider('answerProvider')] public function testSelectChoiceFromChoiceList($providedAnswer, $expectedValue) { $possibleChoices = [ diff --git a/Tests/Helper/SymfonyQuestionHelperTest.php b/Tests/Helper/SymfonyQuestionHelperTest.php index 6cf79965b..56b8f210d 100644 --- a/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/Tests/Helper/SymfonyQuestionHelperTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\Group; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\HelperSet; @@ -20,9 +21,7 @@ use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; -/** - * @group tty - */ +#[Group('tty')] class SymfonyQuestionHelperTest extends AbstractQuestionHelperTestCase { public function testAskChoice() diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index eb85364da..4ab606b54 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Helper; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; @@ -38,9 +39,7 @@ protected function tearDown(): void unset($this->stream); } - /** - * @dataProvider renderProvider - */ + #[DataProvider('renderProvider')] public function testRender($headers, $rows, $style, $expected, $decorated = false) { $table = new Table($output = $this->getOutputStream($decorated)); @@ -54,9 +53,7 @@ public function testRender($headers, $rows, $style, $expected, $decorated = fals $this->assertEquals($expected, $this->getOutputContent($output)); } - /** - * @dataProvider renderProvider - */ + #[DataProvider('renderProvider')] public function testRenderAddRows($headers, $rows, $style, $expected, $decorated = false) { $table = new Table($output = $this->getOutputStream($decorated)); @@ -70,9 +67,7 @@ public function testRenderAddRows($headers, $rows, $style, $expected, $decorated $this->assertEquals($expected, $this->getOutputContent($output)); } - /** - * @dataProvider renderProvider - */ + #[DataProvider('renderProvider')] public function testRenderAddRowsOneByOne($headers, $rows, $style, $expected, $decorated = false) { $table = new Table($output = $this->getOutputStream($decorated)); @@ -1260,9 +1255,7 @@ public function testGetStyleDefinition() Table::getStyleDefinition('absent'); } - /** - * @dataProvider renderSetTitle - */ + #[DataProvider('renderSetTitle')] public function testSetTitle($headerTitle, $footerTitle, $style, $expected) { (new Table($output = $this->getOutputStream())) @@ -1536,9 +1529,7 @@ public static function provideRenderHorizontalTests() yield [$headers, $rows, $expected]; } - /** - * @dataProvider provideRenderHorizontalTests - */ + #[DataProvider('provideRenderHorizontalTests')] public function testRenderHorizontal(array $headers, array $rows, string $expected) { $table = new Table($output = $this->getOutputStream()); @@ -1984,9 +1975,7 @@ public static function provideRenderVerticalTests(): \Traversable ]; } - /** - * @dataProvider provideRenderVerticalTests - */ + #[DataProvider('provideRenderVerticalTests')] public function testVerticalRender(string $expectedOutput, array $headers, array $rows, string $style = 'default', string $headerTitle = '', string $footerTitle = '') { $table = new Table($output = $this->getOutputStream()); diff --git a/Tests/Input/ArgvInputTest.php b/Tests/Input/ArgvInputTest.php index 0e76f9ee6..dead40f46 100644 --- a/Tests/Input/ArgvInputTest.php +++ b/Tests/Input/ArgvInputTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Input; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputArgument; @@ -39,9 +40,7 @@ public function testParseArguments() $this->assertSame(['name' => 'foo'], $input->getArguments(), '->parse() is stateless'); } - /** - * @dataProvider provideOptions - */ + #[DataProvider('provideOptions')] public function testParseOptions($input, $options, $expectedOptions, $message) { $input = new ArgvInput($input); @@ -50,9 +49,7 @@ public function testParseOptions($input, $options, $expectedOptions, $message) $this->assertSame($expectedOptions, $input->getOptions(), $message); } - /** - * @dataProvider provideNegatableOptions - */ + #[DataProvider('provideNegatableOptions')] public function testParseOptionsNegatable($input, $options, $expectedOptions, $message) { $input = new ArgvInput($input); @@ -234,9 +231,7 @@ public static function provideNegatableOptions() ]; } - /** - * @dataProvider provideInvalidInput - */ + #[DataProvider('provideInvalidInput')] public function testInvalidInput($argv, $definition, $expectedExceptionMessage) { $this->expectException(\RuntimeException::class); @@ -245,9 +240,7 @@ public function testInvalidInput($argv, $definition, $expectedExceptionMessage) (new ArgvInput($argv))->bind($definition); } - /** - * @dataProvider provideInvalidNegatableInput - */ + #[DataProvider('provideInvalidNegatableInput')] public function testInvalidInputNegatable($argv, $definition, $expectedExceptionMessage) { $this->expectException(\RuntimeException::class); @@ -509,9 +502,7 @@ public function testToString() $this->assertSame('-f --bar=foo '.escapeshellarg('a b c d').' '.escapeshellarg("A\nB'C"), (string) $input); } - /** - * @dataProvider provideGetParameterOptionValues - */ + #[DataProvider('provideGetParameterOptionValues')] public function testGetParameterOptionEqualSign($argv, $key, $default, $onlyParams, $expected) { $input = new ArgvInput($argv); @@ -574,9 +565,7 @@ public function testGetRawTokensFalse() $this->assertSame(['--foo', 'bar'], $input->getRawTokens()); } - /** - * @dataProvider provideGetRawTokensTrueTests - */ + #[DataProvider('provideGetRawTokensTrueTests')] public function testGetRawTokensTrue(array $argv, array $expected) { $input = new ArgvInput($argv); diff --git a/Tests/Input/ArrayInputTest.php b/Tests/Input/ArrayInputTest.php index 74d2c089f..96bd08b87 100644 --- a/Tests/Input/ArrayInputTest.php +++ b/Tests/Input/ArrayInputTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Input; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; @@ -64,9 +65,7 @@ public function testParseArguments() $this->assertSame(['name' => 'foo'], $input->getArguments(), '->parse() parses required arguments'); } - /** - * @dataProvider provideOptions - */ + #[DataProvider('provideOptions')] public function testParseOptions($input, $options, $expectedOptions, $message) { $input = new ArrayInput($input, new InputDefinition($options)); @@ -122,9 +121,7 @@ public static function provideOptions(): array ]; } - /** - * @dataProvider provideInvalidInput - */ + #[DataProvider('provideInvalidInput')] public function testParseInvalidInput($parameters, $definition, $expectedExceptionMessage) { $this->expectException(\InvalidArgumentException::class); diff --git a/Tests/Input/InputDefinitionTest.php b/Tests/Input/InputDefinitionTest.php index ab203e6e5..3925ec8e5 100644 --- a/Tests/Input/InputDefinitionTest.php +++ b/Tests/Input/InputDefinitionTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Input; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; @@ -359,9 +360,7 @@ public function testGetOptionDefaults() $this->assertSame($defaults, $definition->getOptionDefaults(), '->getOptionDefaults() returns the default values for all options'); } - /** - * @dataProvider getGetSynopsisData - */ + #[DataProvider('getGetSynopsisData')] public function testGetSynopsis(InputDefinition $definition, $expectedSynopsis, $message = null) { $this->assertSame($expectedSynopsis, $definition->getSynopsis(), $message ? '->getSynopsis() '.$message : ''); diff --git a/Tests/Input/StringInputTest.php b/Tests/Input/StringInputTest.php index 92425daab..3a97938a2 100644 --- a/Tests/Input/StringInputTest.php +++ b/Tests/Input/StringInputTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Input; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputDefinition; @@ -19,9 +20,7 @@ class StringInputTest extends TestCase { - /** - * @dataProvider getTokenizeData - */ + #[DataProvider('getTokenizeData')] public function testTokenize($input, $tokens, $message) { $input = new StringInput($input); diff --git a/Tests/Logger/ConsoleLoggerTest.php b/Tests/Logger/ConsoleLoggerTest.php index 0464c8c5f..976ff7a98 100644 --- a/Tests/Logger/ConsoleLoggerTest.php +++ b/Tests/Logger/ConsoleLoggerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Logger; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Log\InvalidArgumentException; use Psr\Log\LoggerInterface; @@ -54,9 +55,7 @@ public function getLogs(): array return $this->output->getLogs(); } - /** - * @dataProvider provideOutputMappingParams - */ + #[DataProvider('provideOutputMappingParams')] public function testOutputMapping($logLevel, $outputVerbosity, $isOutput, $addVerbosityLevelMap = []) { $out = new BufferedOutput($outputVerbosity); @@ -104,9 +103,7 @@ public function testImplements() $this->assertInstanceOf(LoggerInterface::class, $this->getLogger()); } - /** - * @dataProvider provideLevelsAndMessages - */ + #[DataProvider('provideLevelsAndMessages')] public function testLogsAtAllLevels($level, $message) { $logger = $this->getLogger(); diff --git a/Tests/Output/AnsiColorModeTest.php b/Tests/Output/AnsiColorModeTest.php index eb3e463e8..aa1506e26 100644 --- a/Tests/Output/AnsiColorModeTest.php +++ b/Tests/Output/AnsiColorModeTest.php @@ -11,23 +11,20 @@ namespace Symfony\Component\Console\Tests\Output; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Output\AnsiColorMode; class AnsiColorModeTest extends TestCase { - /** - * @dataProvider provideColorsConversion - */ + #[DataProvider('provideColorsConversion')] public function testColorsConversionToAnsi4(string $corlorHex, array $expected) { $this->assertSame((string) $expected[AnsiColorMode::Ansi4->name], AnsiColorMode::Ansi4->convertFromHexToAnsiColorCode($corlorHex)); } - /** - * @dataProvider provideColorsConversion - */ + #[DataProvider('provideColorsConversion')] public function testColorsConversionToAnsi8(string $corlorHex, array $expected) { $this->assertSame('8;5;'.$expected[AnsiColorMode::Ansi8->name], AnsiColorMode::Ansi8->convertFromHexToAnsiColorCode($corlorHex)); diff --git a/Tests/Output/OutputTest.php b/Tests/Output/OutputTest.php index 64e491048..cd3f09680 100644 --- a/Tests/Output/OutputTest.php +++ b/Tests/Output/OutputTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Output; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Output\Output; @@ -94,9 +95,7 @@ private function generateMessages(): iterable yield 'bar'; } - /** - * @dataProvider provideWriteArguments - */ + #[DataProvider('provideWriteArguments')] public function testWriteRawMessage($message, $type, $expectedOutput) { $output = new TestOutput(); @@ -143,9 +142,7 @@ public function testWriteWithInvalidStyle() $this->assertEquals("foo\n", $output->output, '->writeln() do nothing when a style does not exist'); } - /** - * @dataProvider verbosityProvider - */ + #[DataProvider('verbosityProvider')] public function testWriteWithVerbosityOption($verbosity, $expected, $msg) { $output = new TestOutput(); diff --git a/Tests/Question/ChoiceQuestionTest.php b/Tests/Question/ChoiceQuestionTest.php index 564dee724..613cb621f 100644 --- a/Tests/Question/ChoiceQuestionTest.php +++ b/Tests/Question/ChoiceQuestionTest.php @@ -11,14 +11,13 @@ namespace Symfony\Component\Console\Tests\Question; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Question\ChoiceQuestion; class ChoiceQuestionTest extends TestCase { - /** - * @dataProvider selectUseCases - */ + #[DataProvider('selectUseCases')] public function testSelectUseCases($multiSelect, $answers, $expected, $message, $default = null) { $question = new ChoiceQuestion('A question', [ @@ -104,9 +103,7 @@ public function testNonTrimmable() $this->assertSame(['First response ', ' Second response'], $question->getValidator()('First response , Second response')); } - /** - * @dataProvider selectAssociativeChoicesProvider - */ + #[DataProvider('selectAssociativeChoicesProvider')] public function testSelectAssociativeChoices($providedAnswer, $expectedValue) { $question = new ChoiceQuestion('A question', [ diff --git a/Tests/Question/ConfirmationQuestionTest.php b/Tests/Question/ConfirmationQuestionTest.php index bd11047b3..4e3d99fea 100644 --- a/Tests/Question/ConfirmationQuestionTest.php +++ b/Tests/Question/ConfirmationQuestionTest.php @@ -11,14 +11,13 @@ namespace Symfony\Component\Console\Tests\Question; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Question\ConfirmationQuestion; class ConfirmationQuestionTest extends TestCase { - /** - * @dataProvider normalizerUsecases - */ + #[DataProvider('normalizerUsecases')] public function testDefaultRegexUsecases($default, $answers, $expected, $message) { $sut = new ConfirmationQuestion('A question', $default); diff --git a/Tests/Question/QuestionTest.php b/Tests/Question/QuestionTest.php index 15d8212b9..0fc26aed1 100644 --- a/Tests/Question/QuestionTest.php +++ b/Tests/Question/QuestionTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Question; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Question\Question; @@ -44,9 +45,7 @@ public function testGetDefaultDefault() self::assertNull($this->question->getDefault()); } - /** - * @dataProvider providerTrueFalse - */ + #[DataProvider('providerTrueFalse')] public function testIsSetHidden(bool $hidden) { $this->question->setHidden($hidden); @@ -89,9 +88,7 @@ public function testSetHiddenWithNoAutocompleterCallback() $this->assertNull($exception); } - /** - * @dataProvider providerTrueFalse - */ + #[DataProvider('providerTrueFalse')] public function testIsSetHiddenFallback(bool $hidden) { $this->question->setHiddenFallback($hidden); @@ -122,9 +119,7 @@ public static function providerGetSetAutocompleterValues() ]; } - /** - * @dataProvider providerGetSetAutocompleterValues - */ + #[DataProvider('providerGetSetAutocompleterValues')] public function testGetSetAutocompleterValues($values, $expectValues) { $this->question->setAutocompleterValues($values); @@ -143,9 +138,7 @@ public static function providerSetAutocompleterValuesInvalid() ]; } - /** - * @dataProvider providerSetAutocompleterValuesInvalid - */ + #[DataProvider('providerSetAutocompleterValuesInvalid')] public function testSetAutocompleterValuesInvalid($values) { self::expectException(\TypeError::class); @@ -236,9 +229,7 @@ public static function providerGetSetValidator() ]; } - /** - * @dataProvider providerGetSetValidator - */ + #[DataProvider('providerGetSetValidator')] public function testGetSetValidator($callback) { $this->question->setValidator($callback); @@ -255,9 +246,7 @@ public static function providerGetSetMaxAttempts() return [[1], [5], [null]]; } - /** - * @dataProvider providerGetSetMaxAttempts - */ + #[DataProvider('providerGetSetMaxAttempts')] public function testGetSetMaxAttempts($attempts) { $this->question->setMaxAttempts($attempts); @@ -269,9 +258,7 @@ public static function providerSetMaxAttemptsInvalid() return [[0], [-1]]; } - /** - * @dataProvider providerSetMaxAttemptsInvalid - */ + #[DataProvider('providerSetMaxAttemptsInvalid')] public function testSetMaxAttemptsInvalid($attempts) { self::expectException(\InvalidArgumentException::class); @@ -297,9 +284,7 @@ public function testGetNormalizerDefault() self::assertNull($this->question->getNormalizer()); } - /** - * @dataProvider providerTrueFalse - */ + #[DataProvider('providerTrueFalse')] public function testSetMultiline(bool $multiline) { self::assertSame($this->question, $this->question->setMultiline($multiline)); diff --git a/Tests/SignalRegistry/SignalMapTest.php b/Tests/SignalRegistry/SignalMapTest.php index 3a0c49bb0..9bc7f34a2 100644 --- a/Tests/SignalRegistry/SignalMapTest.php +++ b/Tests/SignalRegistry/SignalMapTest.php @@ -11,14 +11,13 @@ namespace Symfony\Component\Console\Tests\SignalRegistry; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\SignalRegistry\SignalMap; class SignalMapTest extends TestCase { - /** - * @requires extension pcntl - */ + #[RequiresPhpExtension('pcntl')] public function testSignalExists() { $this->assertSame('SIGINT', SignalMap::getSignalName(\SIGINT)); diff --git a/Tests/SignalRegistry/SignalRegistryTest.php b/Tests/SignalRegistry/SignalRegistryTest.php index 92d500f9e..51146b3f2 100644 --- a/Tests/SignalRegistry/SignalRegistryTest.php +++ b/Tests/SignalRegistry/SignalRegistryTest.php @@ -11,12 +11,11 @@ namespace Symfony\Component\Console\Tests\SignalRegistry; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\SignalRegistry\SignalRegistry; -/** - * @requires extension pcntl - */ +#[RequiresPhpExtension('pcntl')] class SignalRegistryTest extends TestCase { protected function tearDown(): void diff --git a/Tests/Style/SymfonyStyleTest.php b/Tests/Style/SymfonyStyleTest.php index a3b7ae406..8f82733e6 100644 --- a/Tests/Style/SymfonyStyleTest.php +++ b/Tests/Style/SymfonyStyleTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Style; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; @@ -47,9 +48,7 @@ protected function tearDown(): void putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } - /** - * @dataProvider inputCommandToOutputFilesProvider - */ + #[DataProvider('inputCommandToOutputFilesProvider')] public function testOutputs($inputCommandFilepath, $outputFilepath) { $code = require $inputCommandFilepath; @@ -58,9 +57,7 @@ public function testOutputs($inputCommandFilepath, $outputFilepath) $this->assertStringEqualsFile($outputFilepath, $this->tester->getDisplay(true)); } - /** - * @dataProvider inputInteractiveCommandToOutputFilesProvider - */ + #[DataProvider('inputInteractiveCommandToOutputFilesProvider')] public function testInteractiveOutputs($inputCommandFilepath, $outputFilepath) { $code = require $inputCommandFilepath; diff --git a/Tests/TerminalTest.php b/Tests/TerminalTest.php index d43469d12..4d417dfba 100644 --- a/Tests/TerminalTest.php +++ b/Tests/TerminalTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Output\AnsiColorMode; use Symfony\Component\Console\Terminal; @@ -95,9 +96,7 @@ public function testSttyOnWindows() $this->assertSame((int) $matches[1], $terminal->getWidth()); } - /** - * @dataProvider provideTerminalColorEnv - */ + #[DataProvider('provideTerminalColorEnv')] public function testGetColorMode(?string $testColorTerm, ?string $testTerm, AnsiColorMode $expected) { $oriColorTerm = getenv('COLORTERM'); diff --git a/Tests/Tester/Constraint/CommandIsSuccessfulTest.php b/Tests/Tester/Constraint/CommandIsSuccessfulTest.php index 61ab5d0f8..415f3861f 100644 --- a/Tests/Tester/Constraint/CommandIsSuccessfulTest.php +++ b/Tests/Tester/Constraint/CommandIsSuccessfulTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Tests\Tester\Constraint; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; @@ -27,9 +28,7 @@ public function testConstraint() $this->assertFalse($constraint->evaluate(Command::INVALID, '', true)); } - /** - * @dataProvider providesUnsuccessful - */ + #[DataProvider('providesUnsuccessful')] public function testUnsuccessfulCommand(string $expectedException, int $exitCode) { $constraint = new CommandIsSuccessful(); From c84d2a11373da87564757ea55b24930abbc2413e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Aug 2025 17:50:26 +0200 Subject: [PATCH 23/36] Remove some unneeded var annotations --- Descriptor/ApplicationDescription.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Descriptor/ApplicationDescription.php b/Descriptor/ApplicationDescription.php index 802d68560..ce778c110 100644 --- a/Descriptor/ApplicationDescription.php +++ b/Descriptor/ApplicationDescription.php @@ -85,7 +85,6 @@ private function inspectApplication(): void foreach ($this->sortCommands($all) as $namespace => $commands) { $names = []; - /** @var Command $command */ foreach ($commands as $name => $command) { if (!$command->getName() || (!$this->showHidden && $command->isHidden())) { continue; @@ -104,6 +103,9 @@ private function inspectApplication(): void } } + /** + * @return array> + */ private function sortCommands(array $commands): array { $namespacedCommands = []; From a7bd3b448446c16151788a8bb36c5a2e806a1f83 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 10 Aug 2025 00:28:14 +0200 Subject: [PATCH 24/36] chore: heredoc indentation as of PHP 7.3 https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc --- Command/DumpCompletionCommand.php | 34 +- Command/HelpCommand.php | 12 +- Command/ListCommand.php | 18 +- Tests/Command/ListCommandTest.php | 64 +- Tests/Formatter/OutputFormatterTest.php | 60 +- Tests/Helper/OutputWrapperTest.php | 52 +- Tests/Helper/ProcessHelperTest.php | 54 +- Tests/Helper/QuestionHelperTest.php | 20 +- Tests/Helper/SymfonyQuestionHelperTest.php | 20 +- Tests/Helper/TableTest.php | 1416 ++++++++++---------- Tests/Helper/TreeHelperTest.php | 134 +- Tests/Helper/TreeStyleTest.php | 224 ++-- Tests/Style/SymfonyStyleTest.php | 54 +- 13 files changed, 1081 insertions(+), 1081 deletions(-) diff --git a/Command/DumpCompletionCommand.php b/Command/DumpCompletionCommand.php index 2853fc5f4..1671120a3 100644 --- a/Command/DumpCompletionCommand.php +++ b/Command/DumpCompletionCommand.php @@ -46,33 +46,33 @@ protected function configure(): void $this ->setHelp(<<%command.name% command dumps the shell completion script required -to use shell autocompletion (currently, {$supportedShells} completion are supported). + The %command.name% command dumps the shell completion script required + to use shell autocompletion (currently, {$supportedShells} completion are supported). -Static installation -------------------- + Static installation + ------------------- -Dump the script to a global completion file and restart your shell: + Dump the script to a global completion file and restart your shell: - %command.full_name% {$shell} | sudo tee {$completionFile} + %command.full_name% {$shell} | sudo tee {$completionFile} -Or dump the script to a local file and source it: + Or dump the script to a local file and source it: - %command.full_name% {$shell} > completion.sh + %command.full_name% {$shell} > completion.sh - # source the file whenever you use the project - source completion.sh + # source the file whenever you use the project + source completion.sh - # or add this line at the end of your "{$rcFile}" file: - source /path/to/completion.sh + # or add this line at the end of your "{$rcFile}" file: + source /path/to/completion.sh -Dynamic installation --------------------- + Dynamic installation + -------------------- -Add this to the end of your shell configuration file (e.g. "{$rcFile}"): + Add this to the end of your shell configuration file (e.g. "{$rcFile}"): - eval "$({$fullCommand} completion {$shell})" -EOH + eval "$({$fullCommand} completion {$shell})" + EOH ) ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...)) ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') diff --git a/Command/HelpCommand.php b/Command/HelpCommand.php index a2a72dab4..92acfd24f 100644 --- a/Command/HelpCommand.php +++ b/Command/HelpCommand.php @@ -40,16 +40,16 @@ protected function configure(): void ]) ->setDescription('Display help for a command') ->setHelp(<<<'EOF' -The %command.name% command displays help for a given command: + The %command.name% command displays help for a given command: - %command.full_name% list + %command.full_name% list -You can also output the help in other formats by using the --format option: + You can also output the help in other formats by using the --format option: - %command.full_name% --format=xml list + %command.full_name% --format=xml list -To display the list of available commands, please use the list command. -EOF + To display the list of available commands, please use the list command. + EOF ) ; } diff --git a/Command/ListCommand.php b/Command/ListCommand.php index 61b4b1b3e..e3047e80e 100644 --- a/Command/ListCommand.php +++ b/Command/ListCommand.php @@ -37,22 +37,22 @@ protected function configure(): void ]) ->setDescription('List commands') ->setHelp(<<<'EOF' -The %command.name% command lists all commands: + The %command.name% command lists all commands: - %command.full_name% + %command.full_name% -You can also display the commands for a specific namespace: + You can also display the commands for a specific namespace: - %command.full_name% test + %command.full_name% test -You can also output the information in other formats by using the --format option: + You can also output the information in other formats by using the --format option: - %command.full_name% --format=xml + %command.full_name% --format=xml -It's also possible to get raw list of commands (useful for embedding command runner): + It's also possible to get raw list of commands (useful for embedding command runner): - %command.full_name% --raw -EOF + %command.full_name% --raw + EOF ) ; } diff --git a/Tests/Command/ListCommandTest.php b/Tests/Command/ListCommandTest.php index 7efdb5ab5..916c1dfac 100644 --- a/Tests/Command/ListCommandTest.php +++ b/Tests/Command/ListCommandTest.php @@ -42,11 +42,11 @@ public function testExecuteListsCommandsWithRawOption() $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), '--raw' => true]); $output = <<<'EOF' -completion Dump the shell completion script -help Display help for a command -list List commands + completion Dump the shell completion script + help Display help for a command + list List commands -EOF; + EOF; $this->assertEquals($output, $commandTester->getDisplay(true)); } @@ -59,9 +59,9 @@ public function testExecuteListsCommandsWithNamespaceArgument() $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), 'namespace' => 'foo', '--raw' => true]); $output = <<<'EOF' -foo:bar The foo:bar command + foo:bar The foo:bar command -EOF; + EOF; $this->assertEquals($output, $commandTester->getDisplay(true)); } @@ -74,27 +74,27 @@ public function testExecuteListsCommandsOrder() $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName()], ['decorated' => false]); $output = <<<'EOF' -Console Tool - -Usage: - command [options] [arguments] - -Options: - -h, --help Display help for the given command. When no command is given display help for the list command - --silent Do not output any message - -q, --quiet Only errors are displayed. All other output is suppressed - -V, --version Display this application version - --ansi|--no-ansi Force (or disable --no-ansi) ANSI output - -n, --no-interaction Do not ask any interactive question - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug - -Available commands: - completion Dump the shell completion script - help Display help for a command - list List commands - 0foo - 0foo:bar 0foo:bar command -EOF; + Console Tool + + Usage: + command [options] [arguments] + + Options: + -h, --help Display help for the given command. When no command is given display help for the list command + --silent Do not output any message + -q, --quiet Only errors are displayed. All other output is suppressed + -V, --version Display this application version + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output + -n, --no-interaction Do not ask any interactive question + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + + Available commands: + completion Dump the shell completion script + help Display help for a command + list List commands + 0foo + 0foo:bar 0foo:bar command + EOF; $this->assertEquals($output, trim($commandTester->getDisplay(true))); } @@ -107,11 +107,11 @@ public function testExecuteListsCommandsOrderRaw() $commandTester = new CommandTester($command = $application->get('list')); $commandTester->execute(['command' => $command->getName(), '--raw' => true]); $output = <<<'EOF' -completion Dump the shell completion script -help Display help for a command -list List commands -0foo:bar 0foo:bar command -EOF; + completion Dump the shell completion script + help Display help for a command + list List commands + 0foo:bar 0foo:bar command + EOF; $this->assertEquals($output, trim($commandTester->getDisplay(true))); } diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php index 70c6cc3fb..192a612ce 100644 --- a/Tests/Formatter/OutputFormatterTest.php +++ b/Tests/Formatter/OutputFormatterTest.php @@ -302,49 +302,49 @@ public function testContentWithLineBreaks() $formatter = new OutputFormatter(true); $this->assertEquals(<<format(<<<'EOF' - -some text -EOF + + some text + EOF )); $this->assertEquals(<<format(<<<'EOF' -some text - -EOF + some text + + EOF )); $this->assertEquals(<<format(<<<'EOF' - -some text - -EOF + + some text + + EOF )); $this->assertEquals(<<format(<<<'EOF' - -some text -more text - -EOF + + some text + more text + + EOF )); } diff --git a/Tests/Helper/OutputWrapperTest.php b/Tests/Helper/OutputWrapperTest.php index d09fd9ea9..e55221048 100644 --- a/Tests/Helper/OutputWrapperTest.php +++ b/Tests/Helper/OutputWrapperTest.php @@ -34,18 +34,18 @@ public static function textProvider(): iterable 20, false, <<<'EOS' - Árvíztűrőtükörfúrógé - p https://github.com/symfony/symfony Lorem ipsum - dolor sit amet, - consectetur - adipiscing elit. - Praesent vestibulum - nulla quis urna - maximus porttitor. - Donec ullamcorper - risus at libero - ornare efficitur. - EOS, + Árvíztűrőtükörfúrógé + p https://github.com/symfony/symfony Lorem ipsum + dolor sit amet, + consectetur + adipiscing elit. + Praesent vestibulum + nulla quis urna + maximus porttitor. + Donec ullamcorper + risus at libero + ornare efficitur. + EOS, ]; yield 'Allow URL cut' => [ @@ -53,20 +53,20 @@ public static function textProvider(): iterable 20, true, <<<'EOS' - Árvíztűrőtükörfúrógé - p - https://github.com/s - ymfony/symfony Lorem - ipsum dolor sit - amet, consectetur - adipiscing elit. - Praesent vestibulum - nulla quis urna - maximus porttitor. - Donec ullamcorper - risus at libero - ornare efficitur. - EOS, + Árvíztűrőtükörfúrógé + p + https://github.com/s + ymfony/symfony Lorem + ipsum dolor sit + amet, consectetur + adipiscing elit. + Praesent vestibulum + nulla quis urna + maximus porttitor. + Donec ullamcorper + risus at libero + ornare efficitur. + EOS, ]; } } diff --git a/Tests/Helper/ProcessHelperTest.php b/Tests/Helper/ProcessHelperTest.php index 02d9fb939..38a6d543f 100644 --- a/Tests/Helper/ProcessHelperTest.php +++ b/Tests/Helper/ProcessHelperTest.php @@ -51,48 +51,48 @@ public function testPassedCallbackIsExecuted() public static function provideCommandsAndOutput(): array { $successOutputVerbose = <<<'EOT' - RUN php -r "echo 42;" - RES Command ran successfully + RUN php -r "echo 42;" + RES Command ran successfully -EOT; + EOT; $successOutputDebug = <<<'EOT' - RUN php -r "echo 42;" - OUT 42 - RES Command ran successfully + RUN php -r "echo 42;" + OUT 42 + RES Command ran successfully -EOT; + EOT; $successOutputDebugWithTags = <<<'EOT' - RUN php -r "echo '42';" - OUT 42 - RES Command ran successfully + RUN php -r "echo '42';" + OUT 42 + RES Command ran successfully -EOT; + EOT; $successOutputProcessDebug = <<<'EOT' - RUN 'php' '-r' 'echo 42;' - OUT 42 - RES Command ran successfully + RUN 'php' '-r' 'echo 42;' + OUT 42 + RES Command ran successfully -EOT; + EOT; $syntaxErrorOutputVerbose = <<<'EOT' - RUN php -r "fwrite(STDERR, 'error message');usleep(50000);fwrite(STDOUT, 'out message');exit(252);" - RES 252 Command did not run successfully + RUN php -r "fwrite(STDERR, 'error message');usleep(50000);fwrite(STDOUT, 'out message');exit(252);" + RES 252 Command did not run successfully -EOT; + EOT; $syntaxErrorOutputDebug = <<<'EOT' - RUN php -r "fwrite(STDERR, 'error message');usleep(500000);fwrite(STDOUT, 'out message');exit(252);" - ERR error message - OUT out message - RES 252 Command did not run successfully + RUN php -r "fwrite(STDERR, 'error message');usleep(500000);fwrite(STDOUT, 'out message');exit(252);" + ERR error message + OUT out message + RES 252 Command did not run successfully -EOT; + EOT; $PHP = '\\' === \DIRECTORY_SEPARATOR ? '"!PHP!"' : '"$PHP"'; $successOutputPhp = <<getInputStream($essay); @@ -503,11 +503,11 @@ public function testAskMultilineResponseWithMultipleNewlinesAtEnd() public function testAskMultilineResponseWithWithCursorInMiddleOfSeekableInputStream() { $input = <<getInputStream($input); fseek($response, 8); diff --git a/Tests/Helper/SymfonyQuestionHelperTest.php b/Tests/Helper/SymfonyQuestionHelperTest.php index 56b8f210d..745633c3b 100644 --- a/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/Tests/Helper/SymfonyQuestionHelperTest.php @@ -154,12 +154,12 @@ public function testChoiceQuestionPadding() ); $this->assertOutputContains(<< -EOT + qqq: + [foo ] foo + [żółw ] bar + [łabądź] baz + > + EOT , $output, true); } @@ -175,10 +175,10 @@ public function testChoiceQuestionCustomPrompt() ); $this->assertOutputContains(<<ccc> -EOT + qqq: + [0] foo + >ccc> + EOT , $output, true); } diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 4ab606b54..51a558a81 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -98,30 +98,30 @@ public static function renderProvider() $books, 'default', <<<'TABLE' -+---------------+--------------------------+------------------+ -| ISBN | Title | Author | -+---------------+--------------------------+------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | -+---------------+--------------------------+------------------+ - -TABLE, + +---------------+--------------------------+------------------+ + | ISBN | Title | Author | + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +---------------+--------------------------+------------------+ + + TABLE, ], [ ['ISBN', 'Title', 'Author'], $books, 'markdown', <<<'TABLE' -| ISBN | Title | Author | -|---------------|--------------------------|------------------| -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | - -TABLE, + | ISBN | Title | Author | + |---------------|--------------------------|------------------| + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + + TABLE, ], [ ['ISBN', 'Title', 'Author'], @@ -157,16 +157,16 @@ public static function renderProvider() $books, 'box', <<<'TABLE' -┌───────────────┬──────────────────────────┬──────────────────┐ -│ ISBN │ Title │ Author │ -├───────────────┼──────────────────────────┼──────────────────┤ -│ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │ -│ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │ -│ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │ -│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ -└───────────────┴──────────────────────────┴──────────────────┘ - -TABLE, + ┌───────────────┬──────────────────────────┬──────────────────┐ + │ ISBN │ Title │ Author │ + ├───────────────┼──────────────────────────┼──────────────────┤ + │ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │ + │ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │ + │ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │ + │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ + └───────────────┴──────────────────────────┴──────────────────┘ + + TABLE, ], [ ['ISBN', 'Title', 'Author'], @@ -179,17 +179,17 @@ public static function renderProvider() ], 'box-double', <<<'TABLE' -╔═══════════════╤══════════════════════════╤══════════════════╗ -║ ISBN │ Title │ Author ║ -╠═══════════════╪══════════════════════════╪══════════════════╣ -║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ -║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ -╟───────────────┼──────────────────────────┼──────────────────╢ -║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ -║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ -╚═══════════════╧══════════════════════════╧══════════════════╝ - -TABLE, + ╔═══════════════╤══════════════════════════╤══════════════════╗ + ║ ISBN │ Title │ Author ║ + ╠═══════════════╪══════════════════════════╪══════════════════╣ + ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + ╟───────────────┼──────────────────────────┼──────────────────╢ + ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + ╚═══════════════╧══════════════════════════╧══════════════════╝ + + TABLE, ], [ ['ISBN', 'Title'], @@ -201,16 +201,16 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+--------------------------+------------------+ -| ISBN | Title | | -+---------------+--------------------------+------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | | | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | -+---------------+--------------------------+------------------+ - -TABLE, + +---------------+--------------------------+------------------+ + | ISBN | Title | | + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | | | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +---------------+--------------------------+------------------+ + + TABLE, ], [ [], @@ -222,14 +222,14 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+--------------------------+------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | | | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | -+---------------+--------------------------+------------------+ - -TABLE, + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | | | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +---------------+--------------------------+------------------+ + + TABLE, ], [ ['ISBN', 'Title', 'Author'], @@ -241,31 +241,31 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+----------------------------+-----------------+ -| ISBN | Title | Author | -+---------------+----------------------------+-----------------+ -| 99921-58-10-7 | Divine | Dante Alighieri | -| | Comedy | | -| 9971-5-0210-2 | Harry Potter | Rowling | -| | and the Chamber of Secrets | Joanne K. | -| 9971-5-0210-2 | Harry Potter | Rowling | -| | and the Chamber of Secrets | Joanne K. | -| 960-425-059-0 | The Lord of the Rings | J. R. R. | -| | | Tolkien | -+---------------+----------------------------+-----------------+ - -TABLE, + +---------------+----------------------------+-----------------+ + | ISBN | Title | Author | + +---------------+----------------------------+-----------------+ + | 99921-58-10-7 | Divine | Dante Alighieri | + | | Comedy | | + | 9971-5-0210-2 | Harry Potter | Rowling | + | | and the Chamber of Secrets | Joanne K. | + | 9971-5-0210-2 | Harry Potter | Rowling | + | | and the Chamber of Secrets | Joanne K. | + | 960-425-059-0 | The Lord of the Rings | J. R. R. | + | | | Tolkien | + +---------------+----------------------------+-----------------+ + + TABLE, ], [ ['ISBN', 'Title'], [], 'default', <<<'TABLE' -+------+-------+ -| ISBN | Title | -+------+-------+ + +------+-------+ + | ISBN | Title | + +------+-------+ -TABLE, + TABLE, ], [ [], @@ -281,14 +281,14 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+----------------------+-----------------+ -| ISBN | Title | Author | -+---------------+----------------------+-----------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -+---------------+----------------------+-----------------+ - -TABLE, + +---------------+----------------------+-----------------+ + | ISBN | Title | Author | + +---------------+----------------------+-----------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + +---------------+----------------------+-----------------+ + + TABLE, ], 'Cell text with tags not used for Output styling' => [ ['ISBN', 'Title', 'Author'], @@ -298,14 +298,14 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+----------------------------------+----------------------+-----------------+ -| ISBN | Title | Author | -+----------------------------------+----------------------+-----------------+ -| 99921-58-10-700 | Divine Com | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -+----------------------------------+----------------------+-----------------+ - -TABLE, + +----------------------------------+----------------------+-----------------+ + | ISBN | Title | Author | + +----------------------------------+----------------------+-----------------+ + | 99921-58-10-700 | Divine Com | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + +----------------------------------+----------------------+-----------------+ + + TABLE, ], 'Cell with colspan' => [ ['ISBN', 'Title', 'Author'], @@ -330,22 +330,22 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+-------------------------------+-------------------------------+-----------------------------+ -| ISBN | Title | Author | -+-------------------------------+-------------------------------+-----------------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -+-------------------------------+-------------------------------+-----------------------------+ -| Divine Comedy(Dante Alighieri) | -+-------------------------------+-------------------------------+-----------------------------+ -| Arduino: A Quick-Start Guide | Mark Schmidt | -+-------------------------------+-------------------------------+-----------------------------+ -| 9971-5-0210-0 | A Tale of | -| | Two Cities | -+-------------------------------+-------------------------------+-----------------------------+ -| Cupìdĭtâte díctá âtquè pôrrò, tèmpórà exercitátìónèm mòdí ânìmí núllà nèmò vèl níhìl! | -+-------------------------------+-------------------------------+-----------------------------+ - -TABLE, + +-------------------------------+-------------------------------+-----------------------------+ + | ISBN | Title | Author | + +-------------------------------+-------------------------------+-----------------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + +-------------------------------+-------------------------------+-----------------------------+ + | Divine Comedy(Dante Alighieri) | + +-------------------------------+-------------------------------+-----------------------------+ + | Arduino: A Quick-Start Guide | Mark Schmidt | + +-------------------------------+-------------------------------+-----------------------------+ + | 9971-5-0210-0 | A Tale of | + | | Two Cities | + +-------------------------------+-------------------------------+-----------------------------+ + | Cupìdĭtâte díctá âtquè pôrrò, tèmpórà exercitátìónèm mòdí ânìmí núllà nèmò vèl níhìl! | + +-------------------------------+-------------------------------+-----------------------------+ + + TABLE, ], 'Cell after colspan contains new line break' => [ ['Foo', 'Bar', 'Baz'], @@ -357,14 +357,14 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+-----+-----+-----+ -| Foo | Bar | Baz | -+-----+-----+-----+ -| foo | baz | -| bar | qux | -+-----+-----+-----+ - -TABLE, + +-----+-----+-----+ + | Foo | Bar | Baz | + +-----+-----+-----+ + | foo | baz | + | bar | qux | + +-----+-----+-----+ + + TABLE, ], 'Cell after colspan contains multiple new lines' => [ ['Foo', 'Bar', 'Baz'], @@ -376,15 +376,15 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+-----+-----+------+ -| Foo | Bar | Baz | -+-----+-----+------+ -| foo | baz | -| bar | qux | -| | quux | -+-----+-----+------+ - -TABLE, + +-----+-----+------+ + | Foo | Bar | Baz | + +-----+-----+------+ + | foo | baz | + | bar | qux | + | | quux | + +-----+-----+------+ + + TABLE, ], 'Cell with rowspan' => [ ['ISBN', 'Title', 'Author'], @@ -402,20 +402,20 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+---------------+-----------------+ -| ISBN | Title | Author | -+---------------+---------------+-----------------+ -| 9971-5-0210-0 | Divine Comedy | Dante Alighieri | -| | | | -| | The Lord of | J. R. | -| | the Rings | R. Tolkien | -+---------------+---------------+-----------------+ -| 80-902734-1-6 | And Then | Agatha Christie | -| 80-902734-1-7 | There | Test | -| | Were None | | -+---------------+---------------+-----------------+ - -TABLE, + +---------------+---------------+-----------------+ + | ISBN | Title | Author | + +---------------+---------------+-----------------+ + | 9971-5-0210-0 | Divine Comedy | Dante Alighieri | + | | | | + | | The Lord of | J. R. | + | | the Rings | R. Tolkien | + +---------------+---------------+-----------------+ + | 80-902734-1-6 | And Then | Agatha Christie | + | 80-902734-1-7 | There | Test | + | | Were None | | + +---------------+---------------+-----------------+ + + TABLE, ], 'Cell with rowspan and colspan' => [ ['ISBN', 'Title', 'Author'], @@ -435,18 +435,18 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+------------------+---------+-----------------+ -| ISBN | Title | Author | -+------------------+---------+-----------------+ -| 9971-5-0210-0 | Dante Alighieri | -| | Charles Dickens | -+------------------+---------+-----------------+ -| Dante Alighieri | 9971-5-0210-0 | -| J. R. R. Tolkien | | -| J. R. R | | -+------------------+---------+-----------------+ - -TABLE, + +------------------+---------+-----------------+ + | ISBN | Title | Author | + +------------------+---------+-----------------+ + | 9971-5-0210-0 | Dante Alighieri | + | | Charles Dickens | + +------------------+---------+-----------------+ + | Dante Alighieri | 9971-5-0210-0 | + | J. R. R. Tolkien | | + | J. R. R | | + +------------------+---------+-----------------+ + + TABLE, ], 'Cell with rowspan and colspan contains new line break' => [ ['ISBN', 'Title', 'Author'], @@ -470,26 +470,26 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+-----------------+-------+-----------------+ -| ISBN | Title | Author | -+-----------------+-------+-----------------+ -| 9971 | Dante Alighieri | -| -5- | Charles Dickens | -| 021 | | -| 0-0 | | -+-----------------+-------+-----------------+ -| Dante Alighieri | 9971 | -| Charles Dickens | -5- | -| | 021 | -| | 0-0 | -+-----------------+-------+-----------------+ -| 9971 | Dante | -| -5- | Alighieri | -| 021 | | -| 0-0 | | -+-----------------+-------+-----------------+ - -TABLE, + +-----------------+-------+-----------------+ + | ISBN | Title | Author | + +-----------------+-------+-----------------+ + | 9971 | Dante Alighieri | + | -5- | Charles Dickens | + | 021 | | + | 0-0 | | + +-----------------+-------+-----------------+ + | Dante Alighieri | 9971 | + | Charles Dickens | -5- | + | | 021 | + | | 0-0 | + +-----------------+-------+-----------------+ + | 9971 | Dante | + | -5- | Alighieri | + | 021 | | + | 0-0 | | + +-----------------+-------+-----------------+ + + TABLE, ], 'Cell with rowspan and colspan without using TableSeparator' => [ ['ISBN', 'Title', 'Author'], @@ -507,20 +507,20 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+-----------------+-------+-----------------+ -| ISBN | Title | Author | -+-----------------+-------+-----------------+ -| 9971 | Dante Alighieri | -| -5- | Charles Dickens | -| 021 | | -| 0-0 | | -| Dante Alighieri | 9971 | -| Charles Dickens | -5- | -| | 021 | -| | 0-0 | -+-----------------+-------+-----------------+ - -TABLE, + +-----------------+-------+-----------------+ + | ISBN | Title | Author | + +-----------------+-------+-----------------+ + | 9971 | Dante Alighieri | + | -5- | Charles Dickens | + | 021 | | + | 0-0 | | + | Dante Alighieri | 9971 | + | Charles Dickens | -5- | + | | 021 | + | | 0-0 | + +-----------------+-------+-----------------+ + + TABLE, ], 'Cell with rowspan and colspan with separator inside a rowspan' => [ ['ISBN', 'Author'], @@ -534,15 +534,15 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+-----------------+ -| ISBN | Author | -+---------------+-----------------+ -| 9971-5-0210-0 | Dante Alighieri | -| |-----------------| -| | Charles Dickens | -+---------------+-----------------+ - -TABLE, + +---------------+-----------------+ + | ISBN | Author | + +---------------+-----------------+ + | 9971-5-0210-0 | Dante Alighieri | + | |-----------------| + | | Charles Dickens | + +---------------+-----------------+ + + TABLE, ], 'Multiple header lines' => [ [ @@ -552,13 +552,13 @@ public static function renderProvider() [], 'default', <<<'TABLE' -+------+-------+--------+ -| Main title | -+------+-------+--------+ -| ISBN | Title | Author | -+------+-------+--------+ + +------+-------+--------+ + | Main title | + +------+-------+--------+ + | ISBN | Title | Author | + +------+-------+--------+ -TABLE, + TABLE, ], 'Row with multiple cells' => [ [], @@ -572,11 +572,11 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---+--+--+---+--+---+--+---+--+ -| 1 | 2 | 3 | 4 | -+---+--+--+---+--+---+--+---+--+ + +---+--+--+---+--+---+--+---+--+ + | 1 | 2 | 3 | 4 | + +---+--+--+---+--+---+--+---+--+ -TABLE, + TABLE, ], 'Coslpan and table cells with comment style' => [ [ @@ -595,15 +595,15 @@ public static function renderProvider() ], 'default', << 1' => [ @@ -775,15 +775,15 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+---------------+---------------+-------------------------------------------+ -| 978 | De Monarchia | Dante Alighieri | -| 99921-58-10-7 | Divine Comedy | spans multiple rows rows Dante Alighieri | -| | | spans multiple rows rows | -+---------------+---------------+-------------------------------------------+ -| test | tttt | -+---------------+---------------+-------------------------------------------+ - -TABLE + +---------------+---------------+-------------------------------------------+ + | 978 | De Monarchia | Dante Alighieri | + | 99921-58-10-7 | Divine Comedy | spans multiple rows rows Dante Alighieri | + | | | spans multiple rows rows | + +---------------+---------------+-------------------------------------------+ + | test | tttt | + +---------------+---------------+-------------------------------------------+ + + TABLE , true, ], @@ -830,15 +830,15 @@ public static function renderProvider() ], 'default', <<<'TABLE' -+----------------+---------------+---------------------+ -| ISBN | Title | Author | -+----------------+---------------+---------------------+ -| 978-0521567817 | De Monarchia | Dante Alighieri | -| 978-0804169127 | Divine Comedy | spans multiple rows | -| test | tttt | -+----------------+---------------+---------------------+ - -TABLE + +----------------+---------------+---------------------+ + | ISBN | Title | Author | + +----------------+---------------+---------------------+ + | 978-0521567817 | De Monarchia | Dante Alighieri | + | 978-0804169127 | Divine Comedy | spans multiple rows | + | test | tttt | + +----------------+---------------+---------------------+ + + TABLE , true, ], @@ -857,13 +857,13 @@ public function testRenderMultiByte() $expected = <<<'TABLE' -+------+ -| ■■ | -+------+ -| 1234 | -+------+ + +------+ + | ■■ | + +------+ + | 1234 | + +------+ -TABLE; + TABLE; $this->assertEquals($expected, $this->getOutputContent($output)); } @@ -877,11 +877,11 @@ public function testTableCellWithNumericIntValue() $expected = <<<'TABLE' -+-------+ -| 12345 | -+-------+ + +-------+ + | 12345 | + +-------+ -TABLE; + TABLE; $this->assertEquals($expected, $this->getOutputContent($output)); } @@ -895,11 +895,11 @@ public function testTableCellWithNumericFloatValue() $expected = <<<'TABLE' -+----------+ -| 12345.01 | -+----------+ + +----------+ + | 12345.01 | + +----------+ -TABLE; + TABLE; $this->assertEquals($expected, $this->getOutputContent($output)); } @@ -923,13 +923,13 @@ public function testStyle() $expected = <<<'TABLE' -....... -. Foo . -....... -. Bar . -....... + ....... + . Foo . + ....... + . Bar . + ....... -TABLE; + TABLE; $this->assertEquals($expected, $this->getOutputContent($output)); } @@ -950,17 +950,17 @@ public function testRowSeparator() $expected = <<<'TABLE' -+------+ -| Foo | -+------+ -| Bar1 | -+------+ -| Bar2 | -+------+ -| Bar3 | -+------+ - -TABLE; + +------+ + | Foo | + +------+ + | Bar1 | + +------+ + | Bar2 | + +------+ + | Bar3 | + +------+ + + TABLE; $this->assertEquals($expected, $this->getOutputContent($output)); @@ -979,17 +979,17 @@ public function testRenderMultiCalls() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1012,14 +1012,14 @@ public function testColumnStyle() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1058,14 +1058,14 @@ public function testColumnWidth() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1089,14 +1089,14 @@ public function testColumnWidths() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1119,19 +1119,19 @@ public function testSectionOutput() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1152,14 +1152,14 @@ public function testSectionOutputDoesntClearIfTableIsntRendered() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1182,19 +1182,19 @@ public function testSectionOutputWithoutDecoration() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1225,17 +1225,17 @@ public function testSectionOutputHandlesZeroRowsAfterRender() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1283,16 +1283,16 @@ public static function renderSetTitle() 'Page 1/2', 'default', <<<'TABLE' -+---------------+----------- Books --------+------------------+ -| ISBN | Title | Author | -+---------------+--------------------------+------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | -+---------------+--------- Page 1/2 -------+------------------+ - -TABLE + +---------------+----------- Books --------+------------------+ + | ISBN | Title | Author | + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +---------------+--------- Page 1/2 -------+------------------+ + + TABLE , true, ], @@ -1301,50 +1301,50 @@ public static function renderSetTitle() 'footer', 'default', <<<'TABLE' -+---------------+--- Multiline -header -here +------------------+ -| ISBN | Title | Author | -+---------------+--------------------------+------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | -+---------------+---------- footer --------+------------------+ - -TABLE, + +---------------+--- Multiline + header + here +------------------+ + | ISBN | Title | Author | + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +---------------+---------- footer --------+------------------+ + + TABLE, ], [ 'Books', 'Page 1/2', 'box', <<<'TABLE' -┌───────────────┬─────────── Books ────────┬──────────────────┐ -│ ISBN │ Title │ Author │ -├───────────────┼──────────────────────────┼──────────────────┤ -│ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │ -│ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │ -│ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │ -│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ -└───────────────┴───────── Page 1/2 ───────┴──────────────────┘ - -TABLE, + ┌───────────────┬─────────── Books ────────┬──────────────────┐ + │ ISBN │ Title │ Author │ + ├───────────────┼──────────────────────────┼──────────────────┤ + │ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri │ + │ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens │ + │ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien │ + │ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │ + └───────────────┴───────── Page 1/2 ───────┴──────────────────┘ + + TABLE, ], [ 'Boooooooooooooooooooooooooooooooooooooooooooooooooooooooks', 'Page 1/999999999999999999999999999999999999999999999999999', 'default', <<<'TABLE' -+- Booooooooooooooooooooooooooooooooooooooooooooooooooooo... -+ -| ISBN | Title | Author | -+---------------+--------------------------+------------------+ -| 99921-58-10-7 | Divine Comedy | Dante Alighieri | -| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | -| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | -| 80-902734-1-6 | And Then There Were None | Agatha Christie | -+- Page 1/99999999999999999999999999999999999999999999999... -+ - -TABLE, + +- Booooooooooooooooooooooooooooooooooooooooooooooooooooo... -+ + | ISBN | Title | Author | + +---------------+--------------------------+------------------+ + | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + | 80-902734-1-6 | And Then There Were None | Agatha Christie | + +- Page 1/99999999999999999999999999999999999999999999999... -+ + + TABLE, ], ]; } @@ -1360,12 +1360,12 @@ public function testSetTitleWithoutHeaders() ->render(); $expected = <<<'TABLE' -+-------- Reproducer --------+ -| Value | 123-456 | -| Some other value | 789-0 | -+------------------+---------+ + +-------- Reproducer --------+ + | Value | 123-456 | + | Some other value | 789-0 | + +------------------+---------+ -TABLE; + TABLE; $this->assertSame($expected, $this->getOutputContent($output)); } @@ -1385,16 +1385,16 @@ public function testColumnMaxWidths() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1421,15 +1421,15 @@ public function testColumnMaxWidthsHeaders() $expected = <<
assertEquals($expected, $this->getOutputContent($output)); } @@ -1444,12 +1444,12 @@ public function testColumnMaxWidthsWithTrailingBackslash() $expected = <<<'TABLE' -+-------+ -| 1234\ | -| 6 | -+-------+ + +-------+ + | 1234\ | + | 6 | + +-------+ -TABLE; + TABLE; $this->assertEquals($expected, $this->getOutputContent($output)); } @@ -1477,15 +1477,15 @@ public function testBoxedStyleWithColspan() $expected = <<
assertSame($expected, $this->getOutputContent($output)); } @@ -1495,37 +1495,37 @@ public static function provideRenderHorizontalTests() $headers = ['foo', 'bar', 'baz']; $rows = [['one', 'two', 'tree'], ['1', '2', '3']]; $expected = <<assertSame($expected, $this->getOutputContent($output)); } @@ -1616,19 +1616,19 @@ public static function provideRenderVerticalTests(): \Traversable yield 'With header for all' => [ << [ << [ << [ << [ << [ << [ << [ << [ << [ << [ << [ << [ << [ << [ << [ <<assertSame($expected, $this->getOutputContent($output)); } @@ -2041,12 +2041,12 @@ public function testGithubIssue52101HorizontalTrue() $table->render(); $this->assertSame(<<
getOutputContent($output) ); @@ -2070,14 +2070,14 @@ public function testGithubIssue52101HorizontalFalse() $table->render(); $this->assertSame(<<
getOutputContent($output) ); } @@ -2099,16 +2099,16 @@ public function testGithubIssue60038WidthOfCellWithEmoji() $table->render(); $this->assertSame(<<
getOutputContent($output) ); diff --git a/Tests/Helper/TreeHelperTest.php b/Tests/Helper/TreeHelperTest.php index 5d1399b27..36b648751 100644 --- a/Tests/Helper/TreeHelperTest.php +++ b/Tests/Helper/TreeHelperTest.php @@ -52,10 +52,10 @@ public function testRenderTwoLevelTree() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child 1 + └── Child 2 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderThreeLevelTree() @@ -74,11 +74,11 @@ public function testRenderThreeLevelTree() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child 1 + │ └── SubChild 1 + └── Child 2 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderMultiLevelTree() @@ -101,13 +101,13 @@ public function testRenderMultiLevelTree() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child 1 + │ ├── SubChild 1 + │ │ └── SubSubChild 1 + │ └── SubChild 2 + └── Child 2 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderSingleNodeTree() @@ -118,8 +118,8 @@ public function testRenderSingleNodeTree() $tree->render(); $this->assertSame(<<fetch()))); + Root + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderEmptyTree() @@ -130,8 +130,8 @@ public function testRenderEmptyTree() $tree->render(); $this->assertSame(<<fetch()))); + Root + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderDeeplyNestedTree() @@ -158,18 +158,18 @@ public function testRenderDeeplyNestedTree() $tree->render(); $this->assertSame(<<fetch()))); + Root + └── Level 1 + └── Level 2 + └── Level 3 + └── Level 4 + └── Level 5 + └── Level 6 + └── Level 7 + └── Level 8 + └── Level 9 + └── Level 10 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderNodeWithMultipleChildren() @@ -188,11 +188,11 @@ public function testRenderNodeWithMultipleChildren() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child 1 + ├── Child 2 + └── Child 3 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderNodeWithMultipleChildrenWithStringConversion() @@ -208,11 +208,11 @@ public function testRenderNodeWithMultipleChildrenWithStringConversion() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child 1 + ├── Child 2 + └── Child 3 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderTreeWithDuplicateNodeNames() @@ -231,11 +231,11 @@ public function testRenderTreeWithDuplicateNodeNames() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child + │ └── Child + └── Child + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderTreeWithComplexNodeNames() @@ -254,11 +254,11 @@ public function testRenderTreeWithComplexNodeNames() $tree->render(); $this->assertSame(<<fetch()))); + Root + ├── Child 1 (special) + │ └── Node with spaces + └── Child_2@#$ + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRenderTreeWithCycle() @@ -307,10 +307,10 @@ public function testCreateWithRoot() $tree->render(); $this->assertSame(<<fetch()))); + root + ├── child1 + └── child2 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCreateWithNestedArray() @@ -322,14 +322,14 @@ public function testCreateWithNestedArray() $tree->render(); $this->assertSame(<<fetch()))); + root + ├── child1 + ├── child2 + │ ├── child2.1 + │ └── child2.2 + │ └── child2.2.1 + └── child3 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCreateWithoutRoot() @@ -341,9 +341,9 @@ public function testCreateWithoutRoot() $tree->render(); $this->assertSame(<<fetch()))); + ├── child1 + └── child2 + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCreateWithEmptyArray() diff --git a/Tests/Helper/TreeStyleTest.php b/Tests/Helper/TreeStyleTest.php index 7f5bfedd3..78b22d69a 100644 --- a/Tests/Helper/TreeStyleTest.php +++ b/Tests/Helper/TreeStyleTest.php @@ -28,20 +28,20 @@ public function testDefaultStyle() $tree->render(); $this->assertSame(<<fetch()))); + root + ├── A + │ ├── A1 + │ └── A2 + │ └── A2.1 + │ ├── A2.1.1 + │ └── A2.1.2 + ├── B + │ ├── B1 + │ │ ├── B11 + │ │ └── B12 + │ └── B2 + └── C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testBoxStyle() @@ -50,20 +50,20 @@ public function testBoxStyle() $this->createTree($output, TreeStyle::box())->render(); $this->assertSame(<<fetch()))); + root + ┃╸ A + ┃ ┃╸ A1 + ┃ ┗╸ A2 + ┃ ┗╸ A2.1 + ┃ ┃╸ A2.1.1 + ┃ ┗╸ A2.1.2 + ┃╸ B + ┃ ┃╸ B1 + ┃ ┃ ┃╸ B11 + ┃ ┃ ┗╸ B12 + ┃ ┗╸ B2 + ┗╸ C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testBoxDoubleStyle() @@ -72,20 +72,20 @@ public function testBoxDoubleStyle() $this->createTree($output, TreeStyle::boxDouble())->render(); $this->assertSame(<<fetch()))); + root + ╠═ A + ║ ╠═ A1 + ║ ╚═ A2 + ║ ╚═ A2.1 + ║ ╠═ A2.1.1 + ║ ╚═ A2.1.2 + ╠═ B + ║ ╠═ B1 + ║ ║ ╠═ B11 + ║ ║ ╚═ B12 + ║ ╚═ B2 + ╚═ C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCompactStyle() @@ -94,20 +94,20 @@ public function testCompactStyle() $this->createTree($output, TreeStyle::compact())->render(); $this->assertSame(<<<'TREE' -root -├ A -│ ├ A1 -│ └ A2 -│ └ A2.1 -│ ├ A2.1.1 -│ └ A2.1.2 -├ B -│ ├ B1 -│ │ ├ B11 -│ │ └ B12 -│ └ B2 -└ C -TREE, self::normalizeLineBreaks(trim($output->fetch()))); + root + ├ A + │ ├ A1 + │ └ A2 + │ └ A2.1 + │ ├ A2.1.1 + │ └ A2.1.2 + ├ B + │ ├ B1 + │ │ ├ B11 + │ │ └ B12 + │ └ B2 + └ C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testLightStyle() @@ -116,20 +116,20 @@ public function testLightStyle() $this->createTree($output, TreeStyle::light())->render(); $this->assertSame(<<<'TREE' -root -|-- A -| |-- A1 -| `-- A2 -| `-- A2.1 -| |-- A2.1.1 -| `-- A2.1.2 -|-- B -| |-- B1 -| | |-- B11 -| | `-- B12 -| `-- B2 -`-- C -TREE, self::normalizeLineBreaks(trim($output->fetch()))); + root + |-- A + | |-- A1 + | `-- A2 + | `-- A2.1 + | |-- A2.1.1 + | `-- A2.1.2 + |-- B + | |-- B1 + | | |-- B11 + | | `-- B12 + | `-- B2 + `-- C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testMinimalStyle() @@ -138,20 +138,20 @@ public function testMinimalStyle() $this->createTree($output, TreeStyle::minimal())->render(); $this->assertSame(<<<'TREE' -root -. A -. . A1 -. . A2 -. . A2.1 -. . A2.1.1 -. . A2.1.2 -. B -. . B1 -. . . B11 -. . . B12 -. . B2 -. C -TREE, self::normalizeLineBreaks(trim($output->fetch()))); + root + . A + . . A1 + . . A2 + . . A2.1 + . . A2.1.1 + . . A2.1.2 + . B + . . B1 + . . . B11 + . . . B12 + . . B2 + . C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testRoundedStyle() @@ -160,20 +160,20 @@ public function testRoundedStyle() $this->createTree($output, TreeStyle::rounded())->render(); $this->assertSame(<<<'TREE' -root -├─ A -│ ├─ A1 -│ ╰─ A2 -│ ╰─ A2.1 -│ ├─ A2.1.1 -│ ╰─ A2.1.2 -├─ B -│ ├─ B1 -│ │ ├─ B11 -│ │ ╰─ B12 -│ ╰─ B2 -╰─ C -TREE, self::normalizeLineBreaks(trim($output->fetch()))); + root + ├─ A + │ ├─ A1 + │ ╰─ A2 + │ ╰─ A2.1 + │ ├─ A2.1.1 + │ ╰─ A2.1.2 + ├─ B + │ ├─ B1 + │ │ ├─ B11 + │ │ ╰─ B12 + │ ╰─ B2 + ╰─ C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCustomPrefix() @@ -183,20 +183,20 @@ public function testCustomPrefix() self::createTree($output, $style)->render(); $this->assertSame(<<<'TREE' -root -C A F A -C D A F A1 -C D B F A2 -C D E B F A2.1 -C D E E A F A2.1.1 -C D E E B F A2.1.2 -C A F B -C D A F B1 -C D D A F B11 -C D D B F B12 -C D B F B2 -C B F C -TREE, self::normalizeLineBreaks(trim($output->fetch()))); + root + C A F A + C D A F A1 + C D B F A2 + C D E B F A2.1 + C D E E A F A2.1.1 + C D E E B F A2.1.2 + C A F B + C D A F B1 + C D D A F B11 + C D D B F B12 + C D B F B2 + C B F C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } private static function createTree(OutputInterface $output, ?TreeStyle $style = null): TreeHelper diff --git a/Tests/Style/SymfonyStyleTest.php b/Tests/Style/SymfonyStyleTest.php index 8f82733e6..586679288 100644 --- a/Tests/Style/SymfonyStyleTest.php +++ b/Tests/Style/SymfonyStyleTest.php @@ -176,15 +176,15 @@ public function testTree() $tree->render(); $this->assertSame(<<fetch()))); + root + ├── A + ├── B + │ ├── B1 + │ │ ├── B11 + │ │ └── B12 + │ └── B2 + └── C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCreateTreeWithArray() @@ -197,15 +197,15 @@ public function testCreateTreeWithArray() $tree->render(); $this->assertSame($tree = <<fetch()))); + root + ├── A + ├── B + │ ├── B1 + │ │ ├── B11 + │ │ └── B12 + │ └── B2 + └── C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCreateTreeWithIterable() @@ -218,15 +218,15 @@ public function testCreateTreeWithIterable() $tree->render(); $this->assertSame(<<fetch()))); + root + ├── A + ├── B + │ ├── B1 + │ │ ├── B11 + │ │ └── B12 + │ └── B2 + └── C + TREE, self::normalizeLineBreaks(trim($output->fetch()))); } public function testCreateTreeWithConsoleOutput() From 794b7a59f1a1ef98611e593d3446db550c75109c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 1 Aug 2025 14:58:41 +0200 Subject: [PATCH 25/36] run tests with PHPUnit 12.3 --- Tests/Helper/TableTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index 4ab606b54..a657d2dc1 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -1294,7 +1294,6 @@ public static function renderSetTitle() TABLE , - true, ], 'header contains multiple lines' => [ 'Multiline'."\n".'header'."\n".'here', From 7b423f1523ed1a467e1da78fd070de3a4ef96d2a Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Sun, 10 Aug 2025 00:12:49 +0200 Subject: [PATCH 26/36] chore: PHP CS Fixer - update heredoc handling --- Tests/Formatter/OutputFormatterTest.php | 28 +++++---- Tests/Helper/SymfonyQuestionHelperTest.php | 12 ++-- Tests/Helper/TableTest.php | 72 ++++++++-------------- Tests/Helper/TreeHelperTest.php | 52 ++++++++++++---- Tests/Helper/TreeStyleTest.php | 32 +++++++--- Tests/Style/SymfonyStyleTest.php | 12 +++- 6 files changed, 120 insertions(+), 88 deletions(-) diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php index 192a612ce..963ed98b6 100644 --- a/Tests/Formatter/OutputFormatterTest.php +++ b/Tests/Formatter/OutputFormatterTest.php @@ -304,48 +304,52 @@ public function testContentWithLineBreaks() $this->assertEquals(<<format(<<<'EOF' + EOF, + $formatter->format(<<<'EOF' some text EOF - )); + ) + ); $this->assertEquals(<<format(<<<'EOF' + EOF, + $formatter->format(<<<'EOF' some text EOF - )); + ) + ); $this->assertEquals(<<format(<<<'EOF' + EOF, + $formatter->format(<<<'EOF' some text EOF - )); + ) + ); $this->assertEquals(<<format(<<<'EOF' + EOF, + $formatter->format(<<<'EOF' some text more text EOF - )); + ) + ); } public function testFormatAndWrap() diff --git a/Tests/Helper/SymfonyQuestionHelperTest.php b/Tests/Helper/SymfonyQuestionHelperTest.php index 745633c3b..765691d01 100644 --- a/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/Tests/Helper/SymfonyQuestionHelperTest.php @@ -159,8 +159,10 @@ public function testChoiceQuestionPadding() [żółw ] bar [łabądź] baz > - EOT - , $output, true); + EOT, + $output, + true + ); } public function testChoiceQuestionCustomPrompt() @@ -178,8 +180,10 @@ public function testChoiceQuestionCustomPrompt() qqq: [0] foo >ccc> - EOT - , $output, true); + EOT, + $output, + true + ); } protected function getInputStream($input) diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php index e4bcc6d7b..b6f57f560 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -603,8 +603,7 @@ public static function renderProvider() | Dante Alighieri | J. R. R. Tolkien | J. R. R | +-----------------+------------------+---------+ - TABLE - , + TABLE, true, ], 'Row with formatted cells containing a newline' => [ @@ -632,8 +631,7 @@ public static function renderProvider() | bar | here | +-------+------------+ - TABLE - , + TABLE, true, ], 'TabeCellStyle with align. Also with rowspan and colspan > 1' => [ @@ -714,8 +712,7 @@ public static function renderProvider() | test | tttt | +---------------+---------------+-------------------------------------------+ - TABLE - , + TABLE, ], 'TabeCellStyle with fg,bg. Also with rowspan and colspan > 1' => [ [], @@ -783,8 +780,7 @@ public static function renderProvider() | test | tttt | +---------------+---------------+-------------------------------------------+ - TABLE - , + TABLE, true, ], 'TabeCellStyle with cellFormat. Also with rowspan and colspan > 1' => [ @@ -838,8 +834,7 @@ public static function renderProvider() | test | tttt | +----------------+---------------+---------------------+ - TABLE - , + TABLE, true, ], ]; @@ -1292,8 +1287,7 @@ public static function renderSetTitle() | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------- Page 1/2 -------+------------------+ - TABLE - , + TABLE, ], 'header contains multiple lines' => [ 'Multiline'."\n".'header'."\n".'here', @@ -1627,8 +1621,7 @@ public static function provideRenderVerticalTests(): \Traversable | Price: 139.25 | +------------------------------+ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, ]; @@ -1647,8 +1640,7 @@ public static function provideRenderVerticalTests(): \Traversable | 139.25 | +----------------------+ - EOTXT - , + EOTXT, [], $books, ]; @@ -1662,8 +1654,7 @@ public static function provideRenderVerticalTests(): \Traversable | Price: 9.95 | +-------------------------+ - EOTXT - , + EOTXT, ['ISBN', 'Títle', 'Author', 'Price'], [ [ @@ -1689,8 +1680,7 @@ public static function provideRenderVerticalTests(): \Traversable | : 139.25 | +------------------------------+ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author'], $books, ]; @@ -1707,8 +1697,7 @@ public static function provideRenderVerticalTests(): \Traversable | baz: | +----------+ - EOTXT - , + EOTXT, ['foo', 'bar', 'baz'], [ ['one', 'two'], @@ -1728,8 +1717,7 @@ public static function provideRenderVerticalTests(): \Traversable | baz: 3 | +-----------+ - EOTXT - , + EOTXT, ['foo', 'bar', 'baz'], [ ['one', 'two', 'tree'], @@ -1753,8 +1741,7 @@ public static function provideRenderVerticalTests(): \Traversable | Price: 139.25 | +-------------------------+ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], [ ['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'], @@ -1774,8 +1761,7 @@ public static function provideRenderVerticalTests(): \Traversable | Author: Charles Dickens | +------------------------------+ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author'], [ ['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'], @@ -1797,8 +1783,7 @@ public static function provideRenderVerticalTests(): \Traversable | Author: Charles Dickens | +---------------------------------------------------------------------------------------+ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author'], [ ['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'], @@ -1829,8 +1814,7 @@ public static function provideRenderVerticalTests(): \Traversable | Lorem ipsum dolor sit amet, consectetur | +--------------------------------------------------------------------------------+ - EOTXT - , + EOTXT, [], [ [new TableCell('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor', ['colspan' => 3])], @@ -1861,8 +1845,7 @@ public static function provideRenderVerticalTests(): \Traversable Price: 139.25 ============================== - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, 'borderless', @@ -1880,8 +1863,7 @@ public static function provideRenderVerticalTests(): \Traversable Author: Charles Dickens Price: 139.25 - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, 'compact', @@ -1901,8 +1883,7 @@ public static function provideRenderVerticalTests(): \Traversable Price: 139.25 ------------------------------ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, 'symfony-style-guide', @@ -1922,8 +1903,7 @@ public static function provideRenderVerticalTests(): \Traversable │ Price: 139.25 │ └──────────────────────────────┘ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, 'box', @@ -1943,8 +1923,7 @@ public static function provideRenderVerticalTests(): \Traversable ║ Price: 139.25 ║ ╚══════════════════════════════╝ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, 'box-double', @@ -1964,8 +1943,7 @@ public static function provideRenderVerticalTests(): \Traversable | Price: 139.25 | +---------- Page 1/2 ----------+ - EOTXT - , + EOTXT, ['ISBN', 'Title', 'Author', 'Price'], $books, 'default', @@ -2045,8 +2023,7 @@ public function testGithubIssue52101HorizontalTrue() │ World │ 2 │ 4 │ └───────┴───┴───┘ - TABLE - , + TABLE, $this->getOutputContent($output) ); } @@ -2107,8 +2084,7 @@ public function testGithubIssue60038WidthOfCellWithEmoji() | And a very long line to show difference in previous lines | | +-----------------------------------------------------------+--------------------+ - TABLE - , + TABLE, $this->getOutputContent($output) ); } diff --git a/Tests/Helper/TreeHelperTest.php b/Tests/Helper/TreeHelperTest.php index 36b648751..cd3bbd691 100644 --- a/Tests/Helper/TreeHelperTest.php +++ b/Tests/Helper/TreeHelperTest.php @@ -55,7 +55,9 @@ public function testRenderTwoLevelTree() Root ├── Child 1 └── Child 2 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderThreeLevelTree() @@ -78,7 +80,9 @@ public function testRenderThreeLevelTree() ├── Child 1 │ └── SubChild 1 └── Child 2 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderMultiLevelTree() @@ -107,7 +111,9 @@ public function testRenderMultiLevelTree() │ │ └── SubSubChild 1 │ └── SubChild 2 └── Child 2 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderSingleNodeTree() @@ -119,7 +125,9 @@ public function testRenderSingleNodeTree() $tree->render(); $this->assertSame(<<fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderEmptyTree() @@ -131,7 +139,9 @@ public function testRenderEmptyTree() $tree->render(); $this->assertSame(<<fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderDeeplyNestedTree() @@ -169,7 +179,9 @@ public function testRenderDeeplyNestedTree() └── Level 8 └── Level 9 └── Level 10 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderNodeWithMultipleChildren() @@ -192,7 +204,9 @@ public function testRenderNodeWithMultipleChildren() ├── Child 1 ├── Child 2 └── Child 3 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderNodeWithMultipleChildrenWithStringConversion() @@ -212,7 +226,9 @@ public function testRenderNodeWithMultipleChildrenWithStringConversion() ├── Child 1 ├── Child 2 └── Child 3 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderTreeWithDuplicateNodeNames() @@ -235,7 +251,9 @@ public function testRenderTreeWithDuplicateNodeNames() ├── Child │ └── Child └── Child - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderTreeWithComplexNodeNames() @@ -258,7 +276,9 @@ public function testRenderTreeWithComplexNodeNames() ├── Child 1 (special) │ └── Node with spaces └── Child_2@#$ - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRenderTreeWithCycle() @@ -310,7 +330,9 @@ public function testCreateWithRoot() root ├── child1 └── child2 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCreateWithNestedArray() @@ -329,7 +351,9 @@ public function testCreateWithNestedArray() │ └── child2.2 │ └── child2.2.1 └── child3 - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCreateWithoutRoot() @@ -343,7 +367,9 @@ public function testCreateWithoutRoot() $this->assertSame(<<fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCreateWithEmptyArray() diff --git a/Tests/Helper/TreeStyleTest.php b/Tests/Helper/TreeStyleTest.php index 78b22d69a..eadb45ded 100644 --- a/Tests/Helper/TreeStyleTest.php +++ b/Tests/Helper/TreeStyleTest.php @@ -41,7 +41,9 @@ public function testDefaultStyle() │ │ └── B12 │ └── B2 └── C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testBoxStyle() @@ -63,7 +65,9 @@ public function testBoxStyle() ┃ ┃ ┗╸ B12 ┃ ┗╸ B2 ┗╸ C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testBoxDoubleStyle() @@ -85,7 +89,9 @@ public function testBoxDoubleStyle() ║ ║ ╚═ B12 ║ ╚═ B2 ╚═ C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCompactStyle() @@ -107,7 +113,9 @@ public function testCompactStyle() │ │ └ B12 │ └ B2 └ C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testLightStyle() @@ -129,7 +137,9 @@ public function testLightStyle() | | `-- B12 | `-- B2 `-- C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testMinimalStyle() @@ -151,7 +161,9 @@ public function testMinimalStyle() . . . B12 . . B2 . C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testRoundedStyle() @@ -173,7 +185,9 @@ public function testRoundedStyle() │ │ ╰─ B12 │ ╰─ B2 ╰─ C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCustomPrefix() @@ -196,7 +210,9 @@ public function testCustomPrefix() C D D B F B12 C D B F B2 C B F C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } private static function createTree(OutputInterface $output, ?TreeStyle $style = null): TreeHelper diff --git a/Tests/Style/SymfonyStyleTest.php b/Tests/Style/SymfonyStyleTest.php index 586679288..e69882004 100644 --- a/Tests/Style/SymfonyStyleTest.php +++ b/Tests/Style/SymfonyStyleTest.php @@ -184,7 +184,9 @@ public function testTree() │ │ └── B12 │ └── B2 └── C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCreateTreeWithArray() @@ -205,7 +207,9 @@ public function testCreateTreeWithArray() │ │ └── B12 │ └── B2 └── C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCreateTreeWithIterable() @@ -226,7 +230,9 @@ public function testCreateTreeWithIterable() │ │ └── B12 │ └── B2 └── C - TREE, self::normalizeLineBreaks(trim($output->fetch()))); + TREE, + self::normalizeLineBreaks(trim($output->fetch())) + ); } public function testCreateTreeWithConsoleOutput() From 1857b9ca8bdb495941bb51b5c1fc966d7314f5ba Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 17 Aug 2025 18:48:21 +0200 Subject: [PATCH 27/36] Use for options in command description --- Command/HelpCommand.php | 2 +- Command/ListCommand.php | 2 +- Tests/Fixtures/application_1.json | 4 ++-- Tests/Fixtures/application_1.xml | 4 ++-- Tests/Fixtures/application_2.json | 4 ++-- Tests/Fixtures/application_2.xml | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Command/HelpCommand.php b/Command/HelpCommand.php index 92acfd24f..a5a54b4e2 100644 --- a/Command/HelpCommand.php +++ b/Command/HelpCommand.php @@ -44,7 +44,7 @@ protected function configure(): void %command.full_name% list - You can also output the help in other formats by using the --format option: + You can also output the help in other formats by using the --format option: %command.full_name% --format=xml list diff --git a/Command/ListCommand.php b/Command/ListCommand.php index e3047e80e..789a8d9ac 100644 --- a/Command/ListCommand.php +++ b/Command/ListCommand.php @@ -45,7 +45,7 @@ protected function configure(): void %command.full_name% test - You can also output the information in other formats by using the --format option: + You can also output the information in other formats by using the --format option: %command.full_name% --format=xml diff --git a/Tests/Fixtures/application_1.json b/Tests/Fixtures/application_1.json index 1477659ad..f57158512 100644 --- a/Tests/Fixtures/application_1.json +++ b/Tests/Fixtures/application_1.json @@ -241,7 +241,7 @@ "help [--format FORMAT] [--raw] [--] []" ], "description": "Display help for a command", - "help": "The help<\/info> command displays help for a given command:\n\n %%PHP_SELF%% help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", + "help": "The help<\/info> command displays help for a given command:\n\n %%PHP_SELF%% help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/info> option:\n\n %%PHP_SELF%% help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", "definition": { "arguments": { "command_name": { @@ -353,7 +353,7 @@ "list [--raw] [--format FORMAT] [--short] [--] []" ], "description": "List commands", - "help": "The list<\/info> command lists all commands:\n\n %%PHP_SELF%% list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n %%PHP_SELF%% list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n %%PHP_SELF%% list --raw<\/info>", + "help": "The list<\/info> command lists all commands:\n\n %%PHP_SELF%% list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n %%PHP_SELF%% list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/info> option:\n\n %%PHP_SELF%% list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n %%PHP_SELF%% list --raw<\/info>", "definition": { "arguments": { "namespace": { diff --git a/Tests/Fixtures/application_1.xml b/Tests/Fixtures/application_1.xml index d726cee35..e1ca6938f 100644 --- a/Tests/Fixtures/application_1.xml +++ b/Tests/Fixtures/application_1.xml @@ -106,7 +106,7 @@ <info>%%PHP_SELF%% help list</info> - You can also output the help in other formats by using the <comment>--format</comment> option: + You can also output the help in other formats by using the <info>--format</info> option: <info>%%PHP_SELF%% help --format=xml list</info> @@ -168,7 +168,7 @@ <info>%%PHP_SELF%% list test</info> - You can also output the information in other formats by using the <comment>--format</comment> option: + You can also output the information in other formats by using the <info>--format</info> option: <info>%%PHP_SELF%% list --format=xml</info> diff --git a/Tests/Fixtures/application_2.json b/Tests/Fixtures/application_2.json index c0e66444e..8876099cb 100644 --- a/Tests/Fixtures/application_2.json +++ b/Tests/Fixtures/application_2.json @@ -245,7 +245,7 @@ "help [--format FORMAT] [--raw] [--] []" ], "description": "Display help for a command", - "help": "The help<\/info> command displays help for a given command:\n\n %%PHP_SELF%% help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", + "help": "The help<\/info> command displays help for a given command:\n\n %%PHP_SELF%% help list<\/info>\n\nYou can also output the help in other formats by using the --format<\/info> option:\n\n %%PHP_SELF%% help --format=xml list<\/info>\n\nTo display the list of available commands, please use the list<\/info> command.", "definition": { "arguments": { "command_name": { @@ -357,7 +357,7 @@ "list [--raw] [--format FORMAT] [--short] [--] []" ], "description": "List commands", - "help": "The list<\/info> command lists all commands:\n\n %%PHP_SELF%% list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n %%PHP_SELF%% list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/comment> option:\n\n %%PHP_SELF%% list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n %%PHP_SELF%% list --raw<\/info>", + "help": "The list<\/info> command lists all commands:\n\n %%PHP_SELF%% list<\/info>\n\nYou can also display the commands for a specific namespace:\n\n %%PHP_SELF%% list test<\/info>\n\nYou can also output the information in other formats by using the --format<\/info> option:\n\n %%PHP_SELF%% list --format=xml<\/info>\n\nIt's also possible to get raw list of commands (useful for embedding command runner):\n\n %%PHP_SELF%% list --raw<\/info>", "definition": { "arguments": { "namespace": { diff --git a/Tests/Fixtures/application_2.xml b/Tests/Fixtures/application_2.xml index dd4b1800a..5ce483ed6 100644 --- a/Tests/Fixtures/application_2.xml +++ b/Tests/Fixtures/application_2.xml @@ -106,7 +106,7 @@ <info>%%PHP_SELF%% help list</info> - You can also output the help in other formats by using the <comment>--format</comment> option: + You can also output the help in other formats by using the <info>--format</info> option: <info>%%PHP_SELF%% help --format=xml list</info> @@ -168,7 +168,7 @@ <info>%%PHP_SELF%% list test</info> - You can also output the information in other formats by using the <comment>--format</comment> option: + You can also output the information in other formats by using the <info>--format</info> option: <info>%%PHP_SELF%% list --format=xml</info> From feba1e4192210386ea7a806864dcff9491b0f17e Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Fri, 8 Aug 2025 19:17:42 -0400 Subject: [PATCH 28/36] [Console] Fix name/alias/usages when an invokable command has an alias --- Command/Command.php | 22 +++++----------------- Tests/Command/CommandTest.php | 10 +++++++++- Tests/Fixtures/InvokableTestCommand.php | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Command/Command.php b/Command/Command.php index 1d2e12bdc..625e9b814 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -89,31 +89,19 @@ public static function getDefaultDescription(): ?string */ public function __construct(?string $name = null, ?callable $code = null) { - $this->definition = new InputDefinition(); - if (null !== $code) { if (!\is_object($code) || $code instanceof \Closure) { throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', self::class)); } - /** @var AsCommand $attribute */ $attribute = ((new \ReflectionObject($code))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance() ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class)); - - $this->setName($name ?? $attribute->name) - ->setDescription($attribute->description ?? '') - ->setHelp($attribute->help ?? '') - ->setCode($code); - - foreach ($attribute->usages as $usage) { - $this->addUsage($usage); - } - - return; + $this->setCode($code); + } else { + $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); } - $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); - + $this->definition = new InputDefinition(); if (null === $name) { if (self::class !== (new \ReflectionMethod($this, 'getDefaultName'))->class) { trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class); @@ -159,7 +147,7 @@ public function __construct(?string $name = null, ?callable $code = null) $this->addUsage($usage); } - if (\is_callable($this) && self::class === (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name) { + if (!$code && \is_callable($this) && self::class === (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name) { $this->code = new InvokableCommand($this, $this(...)); } diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index f07b76a36..867e25c5e 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -304,7 +304,15 @@ public function testRunInteractive() public function testInvokableCommand() { - $tester = new CommandTester(new InvokableTestCommand()); + $invokable = new InvokableTestCommand(); + $command = new Command(null, $invokable); + $this->assertSame('invokable:test', $command->getName()); + $this->assertSame(['inv-test'], $command->getAliases()); + $this->assertSame(['invokable:test usage1', 'invokable:test usage2'], $command->getUsages()); + $this->assertSame('desc', $command->getDescription()); + $this->assertSame('help me', $command->getHelp()); + + $tester = new CommandTester($invokable); $this->assertSame(Command::SUCCESS, $tester->execute([])); } diff --git a/Tests/Fixtures/InvokableTestCommand.php b/Tests/Fixtures/InvokableTestCommand.php index 1fbe32e3c..7c6d75d47 100644 --- a/Tests/Fixtures/InvokableTestCommand.php +++ b/Tests/Fixtures/InvokableTestCommand.php @@ -5,7 +5,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -#[AsCommand('invokable:test')] +#[AsCommand('invokable:test', aliases: ['inv-test'], usages: ['usage1', 'usage2'], description: 'desc', help: 'help me')] class InvokableTestCommand { public function __invoke(): int From 2169188e3a2afe86ed3f25b7ac097b3cb48048c4 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 9 Jul 2025 07:25:15 -0400 Subject: [PATCH 29/36] [Console] Add getter for the original command "code" object --- CHANGELOG.md | 1 + Command/Command.php | 10 ++++++++++ Command/InvokableCommand.php | 15 +++++++++++---- Tests/Command/CommandTest.php | 2 ++ Tests/Command/InvokableCommandTest.php | 9 +++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722045091..b6f2ed70c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.4 --- + * Add `Command::getCode()` to get the code set via `setCode()`. * Allow setting aliases and the hidden flag via the command name passed to the constructor * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` diff --git a/Command/Command.php b/Command/Command.php index 1d2e12bdc..1ae758d6a 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -356,6 +356,16 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti } } + /** + * Gets the code that is executed by the command. + * + * @return ?callable null if the code has not been set with setCode() + */ + public function getCode(): ?callable + { + return $this->code?->getCode(); + } + /** * Sets the code to execute when running this command. * diff --git a/Command/InvokableCommand.php b/Command/InvokableCommand.php index 72ff407c8..b497f4df7 100644 --- a/Command/InvokableCommand.php +++ b/Command/InvokableCommand.php @@ -30,18 +30,20 @@ */ class InvokableCommand implements SignalableCommandInterface { - private readonly \Closure $code; + private readonly \Closure $closure; private readonly ?SignalableCommandInterface $signalableCommand; private readonly \ReflectionFunction $reflection; private bool $triggerDeprecations = false; + private $code; public function __construct( private readonly Command $command, callable $code, ) { - $this->code = $this->getClosure($code); + $this->code = $code; + $this->closure = $this->getClosure($code); $this->signalableCommand = $code instanceof SignalableCommandInterface ? $code : null; - $this->reflection = new \ReflectionFunction($this->code); + $this->reflection = new \ReflectionFunction($this->closure); } /** @@ -49,7 +51,7 @@ public function __construct( */ public function __invoke(InputInterface $input, OutputInterface $output): int { - $statusCode = ($this->code)(...$this->getParameters($input, $output)); + $statusCode = ($this->closure)(...$this->getParameters($input, $output)); if (!\is_int($statusCode)) { if ($this->triggerDeprecations) { @@ -81,6 +83,11 @@ public function configure(InputDefinition $definition): void } } + public function getCode(): callable + { + return $this->code; + } + private function getClosure(callable $code): \Closure { if (!$code instanceof \Closure) { diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php index f07b76a36..c8c9166d2 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -464,6 +464,8 @@ public function testCommandAttribute() $this->assertStringContainsString('usage1', $command->getUsages()[0]); $this->assertTrue($command->isHidden()); $this->assertSame(['f'], $command->getAliases()); + // Standard commands don't have code. + $this->assertNull($command->getCode()); } #[IgnoreDeprecations] diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php index 1eac29944..7f11e7674 100644 --- a/Tests/Command/InvokableCommandTest.php +++ b/Tests/Command/InvokableCommandTest.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; class InvokableCommandTest extends TestCase { @@ -293,6 +294,14 @@ public function __invoke() $command->run(new ArrayInput([]), new NullOutput()); } + public function testGetCode() + { + $invokableTestCommand = new InvokableTestCommand(); + $command = new Command(null, $invokableTestCommand); + + $this->assertSame($invokableTestCommand, $command->getCode()); + } + #[DataProvider('provideInputArguments')] public function testInputArguments(array $parameters, array $expected) { From 263db9fc861af7e6b5953e101f2ccbe6b163435b Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Wed, 9 Jul 2025 07:25:15 -0400 Subject: [PATCH 30/36] [Console] Add getter for the original command "code" object --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f2ed70c..7034aeb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ CHANGELOG 7.4 --- - * Add `Command::getCode()` to get the code set via `setCode()`. + * Add `Command::getCode()` to get the code set via `setCode()` * Allow setting aliases and the hidden flag via the command name passed to the constructor * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` From b088bba049cc843a4e456c1da0127cf5812cb97a Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Sat, 23 Aug 2025 00:01:03 +0200 Subject: [PATCH 31/36] [DI]: removed unnecessary checks on `Definition`s and `Alias`es If it is "public", then for sure it is not "private". https://github.com/symfony/symfony/pull/61505#issuecomment-3227850921 --- DependencyInjection/AddConsoleCommandPass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php index 4a0ee4229..ffbb10e8c 100644 --- a/DependencyInjection/AddConsoleCommandPass.php +++ b/DependencyInjection/AddConsoleCommandPass.php @@ -79,7 +79,7 @@ public function process(ContainerBuilder $container): void } if (null === $commandName) { - if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) { + if ($definition->isPrivate() || $definition->hasTag('container.private')) { $commandId = 'console.command.public_alias.'.$id; $container->setAlias($commandId, $id)->setPublic(true); $id = $commandId; From dc8d57e6d71f26f368f80053b4e5bc92f6c395e5 Mon Sep 17 00:00:00 2001 From: Alexander Kim Date: Thu, 28 Aug 2025 18:29:42 -0400 Subject: [PATCH 32/36] [Console] Add phpdoc for return type of subscribed signals --- Command/SignalableCommandInterface.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Command/SignalableCommandInterface.php b/Command/SignalableCommandInterface.php index 40b301d18..1cf851d90 100644 --- a/Command/SignalableCommandInterface.php +++ b/Command/SignalableCommandInterface.php @@ -20,6 +20,10 @@ interface SignalableCommandInterface { /** * Returns the list of signals to subscribe. + * + * @return list<\SIG*> + * + * @see https://php.net/pcntl.constants for signals */ public function getSubscribedSignals(): array; From cd6aabaab6fb2bf642488d3f769adf24569e329e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=A4u=C3=9Fler?= Date: Sun, 31 Aug 2025 17:45:57 +0200 Subject: [PATCH 33/36] [Console] Harden array type for test-related user inputs Inputs must be strings, otherwise they cannot be properly written to the `php://memory` stream. Using the hardened typed annotation, static code analyzers may assist developers in using correct array values. --- Tester/TesterTrait.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tester/TesterTrait.php b/Tester/TesterTrait.php index 127556d1d..18bdd9a31 100644 --- a/Tester/TesterTrait.php +++ b/Tester/TesterTrait.php @@ -24,6 +24,10 @@ trait TesterTrait { private StreamOutput $output; + + /** + * @var list + */ private array $inputs = []; private bool $captureStreamsIndependently = false; private InputInterface $input; @@ -107,8 +111,8 @@ public function assertCommandIsSuccessful(string $message = ''): void /** * Sets the user inputs. * - * @param array $inputs An array of strings representing each input - * passed to the command input stream + * @param list $inputs An array of strings representing each input + * passed to the command input stream * * @return $this */ @@ -161,6 +165,8 @@ private function initOutput(array $options): void } /** + * @param list $inputs + * * @return resource */ private static function createStream(array $inputs) From 525f69f6840d00b195ffeaea6c3ba1ec6cd87476 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Thu, 21 Aug 2025 01:19:55 +0200 Subject: [PATCH 34/36] [Console] Add #[Input] attribute to support DTOs in commands --- Attribute/Argument.php | 40 +++--- Attribute/Input.php | 116 +++++++++++++++ Attribute/Option.php | 53 +++---- Attribute/Reflection/ReflectionMember.php | 99 +++++++++++++ CHANGELOG.md | 1 + Command/InvokableCommand.php | 29 +++- .../InvokableWithInputTestCommand.php | 77 ++++++++++ Tests/Tester/CommandTesterTest.php | 134 ++++++++++++++++++ 8 files changed, 501 insertions(+), 48 deletions(-) create mode 100644 Attribute/Input.php create mode 100644 Attribute/Reflection/ReflectionMember.php create mode 100644 Tests/Fixtures/InvokableWithInputTestCommand.php diff --git a/Attribute/Argument.php b/Attribute/Argument.php index 203dcc2af..b1b40bd3e 100644 --- a/Attribute/Argument.php +++ b/Attribute/Argument.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\Attribute; +use Symfony\Component\Console\Attribute\Reflection\ReflectionMember; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\InvalidArgumentException; @@ -19,7 +20,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\String\UnicodeString; -#[\Attribute(\Attribute::TARGET_PARAMETER)] +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] class Argument { private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array']; @@ -27,7 +28,9 @@ class Argument private string|bool|int|float|array|null $default = null; private array|\Closure $suggestedValues; private ?int $mode = null; - private string $function = ''; + /** + * @var string|class-string<\BackedEnum> + */ private string $typeName = ''; /** @@ -48,52 +51,45 @@ public function __construct( /** * @internal */ - public static function tryFrom(\ReflectionParameter $parameter): ?self + public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self { - /** @var self $self */ - if (null === $self = ($parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance()) { - return null; - } + $reflection = new ReflectionMember($member); - if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) { - $self->function = $function->class.'::'.$function->name; - } else { - $self->function = $function->name; + if (!$self = $reflection->getAttribute(self::class)) { + return null; } - $type = $parameter->getType(); - $name = $parameter->getName(); + $type = $reflection->getType(); + $name = $reflection->getName(); if (!$type instanceof \ReflectionNamedType) { - throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name, $self->function)); + throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $reflection->getMemberName(), $name, $reflection->getSourceName())); } $self->typeName = $type->getName(); $isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class); if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) { - throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); + throw new LogicException(\sprintf('The type "%s" on %s "$%s" of "%s" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $reflection->getMemberName(), $name, $reflection->getSourceName(), implode('", "', self::ALLOWED_TYPES))); } if (!$self->name) { $self->name = (new UnicodeString($name))->kebab(); } - if ($parameter->isDefaultValueAvailable()) { - $self->default = $parameter->getDefaultValue() instanceof \BackedEnum ? $parameter->getDefaultValue()->value : $parameter->getDefaultValue(); - } + $self->default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null; - $self->mode = $parameter->isDefaultValueAvailable() || $parameter->allowsNull() ? InputArgument::OPTIONAL : InputArgument::REQUIRED; + $self->mode = ($reflection->hasDefaultValue() || $reflection->isNullable()) ? InputArgument::OPTIONAL : InputArgument::REQUIRED; if ('array' === $self->typeName) { $self->mode |= InputArgument::IS_ARRAY; } - if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) { + if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) { $self->suggestedValues = [$instance, $self->suggestedValues[1]]; } if ($isBackedEnum && !$self->suggestedValues) { - $self->suggestedValues = array_column(($self->typeName)::cases(), 'value'); + $self->suggestedValues = array_column($self->typeName::cases(), 'value'); } return $self; @@ -117,7 +113,7 @@ public function resolveValue(InputInterface $input): mixed $value = $input->getArgument($this->name); if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) { - return ($this->typeName)::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues); + return $this->typeName::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues); } return $value; diff --git a/Attribute/Input.php b/Attribute/Input.php new file mode 100644 index 000000000..65bbf4aef --- /dev/null +++ b/Attribute/Input.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +use Symfony\Component\Console\Attribute\Reflection\ReflectionMember; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Input\InputInterface; + +#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)] +final class Input +{ + /** + * @var array + */ + private array $definition = []; + + private \ReflectionClass $class; + + public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self + { + $reflection = new ReflectionMember($member); + + if (!$self = $reflection->getAttribute(self::class)) { + return null; + } + + $type = $reflection->getType(); + + if (!$type instanceof \ReflectionNamedType) { + throw new LogicException(\sprintf('The input %s "%s" must have a named type.', $reflection->getMemberName(), $member->name)); + } + + if (!class_exists($class = $type->getName())) { + throw new LogicException(\sprintf('The input class "%s" does not exist.', $type->getName())); + } + + $self->class = new \ReflectionClass($class); + + foreach ($self->class->getProperties() as $property) { + if (!$property->isPublic() || $property->isStatic()) { + continue; + } + + if ($argument = Argument::tryFrom($property)) { + $self->definition[$property->name] = $argument; + continue; + } + + if ($option = Option::tryFrom($property)) { + $self->definition[$property->name] = $option; + continue; + } + + if ($input = self::tryFrom($property)) { + $self->definition[$property->name] = $input; + } + } + + if (!$self->definition) { + throw new LogicException(\sprintf('The input class "%s" must have at least one argument or option.', $self->class->name)); + } + + return $self; + } + + /** + * @internal + */ + public function resolveValue(InputInterface $input): mixed + { + $instance = $this->class->newInstanceWithoutConstructor(); + + foreach ($this->definition as $name => $spec) { + $instance->$name = $spec->resolveValue($input); + } + + return $instance; + } + + /** + * @return iterable + */ + public function getArguments(): iterable + { + foreach ($this->definition as $spec) { + if ($spec instanceof Argument) { + yield $spec; + } elseif ($spec instanceof self) { + yield from $spec->getArguments(); + } + } + } + + /** + * @return iterable