Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings

Commit 306b82f

Browse filesBrowse the repository at this point in the historyBrowse files
lazergnicolas-grekas
authored andcommitted
[Console] Fix service arguments not resolved when a command is invoked by alias or abbreviation
1 parent ff7ac15 commit 306b82f
Copy full SHA for 306b82f

4 files changed

+104-8Lines changed: 104 additions & 8 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Symfony/Component/Console/ArgumentResolver/ValueResolver/ServiceValueResolver.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/ArgumentResolver/ValueResolver/ServiceValueResolver.php
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public function __construct(
3232

3333
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
3434
{
35-
$command = $input->getFirstArgument();
35+
// the "command" argument is normalized to the resolved command name by Command::run(),
36+
// while getFirstArgument() may return an abbreviation or an alias of it
37+
$command = ($input->hasArgument('command') ? $input->getArgument('command') : null) ?? $input->getFirstArgument();
3638

3739
if ($command && $this->container->has($command)) {
3840
$locator = $this->container->get($command);
Collapse file

‎src/Symfony/Component/Console/Command/Command.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Command/Command.php
+8-7Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ public function run(InputInterface $input, OutputInterface $output): int
242242
}
243243
}
244244

245+
// The command name argument is often omitted when a command is executed directly with its run() method,
246+
// and it may hold an abbreviation or an alias when the command was resolved from one (e.g. Application::find()).
247+
// Normalize it to the command's actual name so it can be relied on afterwards, since it's required by the
248+
// application, and so argument resolution during interact() below can already rely on it.
249+
if ($input->hasArgument('command') && null !== $name = $this->getName()) {
250+
$input->setArgument('command', $name);
251+
}
252+
245253
$this->initialize($input, $output);
246254

247255
if (null !== $this->processTitle) {
@@ -268,13 +276,6 @@ public function run(InputInterface $input, OutputInterface $output): int
268276
}
269277
}
270278

271-
// The command name argument is often omitted when a command is executed directly with its run() method.
272-
// It would fail the validation if we didn't make sure the command argument is present,
273-
// since it's required by the application.
274-
if ($input->hasArgument('command') && null === $input->getArgument('command')) {
275-
$input->setArgument('command', $this->getName());
276-
}
277-
278279
$input->validate();
279280

280281
if ($this->code) {
Collapse file

‎src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php
+61Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPUnit\Framework\TestCase;
1717
use Symfony\Component\Console\Application;
1818
use Symfony\Component\Console\ArgumentResolver\ArgumentResolver;
19+
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ServiceValueResolver;
1920
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
2021
use Symfony\Component\Console\Attribute\Argument;
2122
use Symfony\Component\Console\Attribute\Ask;
@@ -35,10 +36,13 @@
3536
use Symfony\Component\Console\Output\NullOutput;
3637
use Symfony\Component\Console\Output\OutputInterface;
3738
use Symfony\Component\Console\Style\SymfonyStyle;
39+
use Symfony\Component\Console\Tester\ApplicationTester;
3840
use Symfony\Component\Console\Tester\CommandTester;
3941
use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand;
4042
use Symfony\Component\Console\Tests\Fixtures\InvokableWithCustomValidatorTestCommand;
4143
use Symfony\Component\Console\Tests\Fixtures\InvokableWithInputFileAndConstraintsTestCommand;
44+
use Symfony\Component\Console\Tests\Fixtures\InvokableWithServiceArgumentDuringInteractTestCommand;
45+
use Symfony\Component\DependencyInjection\ServiceLocator;
4246

4347
class InvokableCommandTest extends TestCase
4448
{
@@ -493,6 +497,63 @@ public function testHelpersInjection()
493497
$command->run(new ArrayInput([]), new NullOutput());
494498
}
495499

500+
public function testServiceArgumentIsResolvedWhenCommandIsInvokedByAbbreviation()
501+
{
502+
$service = new \stdClass();
503+
504+
$application = new Application();
505+
$application->setArgumentResolver(new ArgumentResolver([
506+
new ServiceValueResolver(new ServiceLocator([
507+
'test:method' => static fn () => new ServiceLocator([
508+
's' => static fn () => $service,
509+
]),
510+
])),
511+
]));
512+
513+
$command = new Command('test:method');
514+
$command->setCode(static function (\stdClass $s) use ($service): int {
515+
Assert::assertSame($service, $s);
516+
517+
return 0;
518+
});
519+
520+
$application->addCommand($command);
521+
$application->setAutoExit(false);
522+
523+
// "t:m" is an abbreviation resolved by Application::find() to "test:method"; the service locator
524+
// registered by RegisterCommandArgumentLocatorsPass is only keyed by the canonical command name,
525+
// so the resolver must not key its lookup off the raw, still-abbreviated user input.
526+
$tester = new ApplicationTester($application);
527+
$tester->run(['command' => 't:m']);
528+
529+
$tester->assertCommandIsSuccessful();
530+
}
531+
532+
public function testServiceArgumentIsResolvedDuringInteractWhenCommandIsInvokedByAbbreviation()
533+
{
534+
$application = new Application();
535+
$application->setArgumentResolver(new ArgumentResolver([
536+
new ServiceValueResolver(new ServiceLocator([
537+
'test:method' => static fn () => new ServiceLocator([
538+
's' => static fn () => new \stdClass(),
539+
]),
540+
])),
541+
]));
542+
543+
$command = new Command('test:method');
544+
$command->setCode(new InvokableWithServiceArgumentDuringInteractTestCommand());
545+
546+
$application->addCommand($command);
547+
$application->setAutoExit(false);
548+
549+
// The #[Interact] method runs before Command::run() gets a chance to validate its input, so the
550+
// "command" argument normalization must happen early enough to also cover this resolution pass.
551+
$tester = new ApplicationTester($application);
552+
$tester->run(['command' => 't:m'], ['interactive' => true]);
553+
554+
$tester->assertCommandIsSuccessful();
555+
}
556+
496557
public function testNullableHelpersInjection()
497558
{
498559
$application = new Application();
Collapse file
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Tests\Fixtures;
13+
14+
use Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Attribute\Interact;
16+
use Symfony\Component\Console\Command\Command;
17+
18+
#[AsCommand('test:method')]
19+
class InvokableWithServiceArgumentDuringInteractTestCommand
20+
{
21+
// Requiring the service here forces argument resolution to run during Command::interact(),
22+
// before the command has ever been validated or invoked.
23+
#[Interact]
24+
public function before(\stdClass $s): void
25+
{
26+
}
27+
28+
public function __invoke(\stdClass $s): int
29+
{
30+
return Command::SUCCESS;
31+
}
32+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.