Skip to content

Commit cebf24a

Browse files
romainguerreroRomain Guerrero
andauthored
Fix FatalError "undefined method Symfony\Component\Console\Application::add()" with the 8.0 Symfony Console version (#353)
* fix: determine which add or addCommand method for Application class should be called depending on the Symfony Console version * test: add tests for the Console/Application class and fix some phpstan issues * test: update phpunit tests to be able to pass on different Symfony versions --------- Co-authored-by: Romain Guerrero <romain.guerrero@acseo.fr>
1 parent e9d1690 commit cebf24a

File tree

3 files changed

+173
-4
lines changed

3 files changed

+173
-4
lines changed

.php-cs-fixer.dist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
$finder = PhpCsFixer\Finder::create()
4-
->exclude('tests/Fixtures/app/cache')
4+
->exclude(['tests/Fixtures/app/cache', 'vendor'])
55
->ignoreDotFiles(false)
66
->in(__DIR__)
77
;

src/Console/Application.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,32 @@ public function __construct(bool $singleCommand = true)
1919

2020
$this->container = new Container();
2121
$command = new LintCommand();
22-
$this->add($command);
23-
$this->add(new RegDebugCommand());
22+
$this->addCommand($command);
23+
$this->addCommand(new RegDebugCommand());
2424

2525
$this->setDefaultCommand($command->getName(), $singleCommand);
2626
}
2727

28-
public function add(Command $command): ?Command
28+
/**
29+
* @param callable|Command $command
30+
*/
31+
public function addCommand($command): ?Command
2932
{
3033
if ($command instanceof ContainerAwareCommand) {
3134
$command->setContainer($this->container);
3235
}
3336

37+
// Symfony Console 8.0+ uses addCommand(), earlier versions use add()
38+
if (method_exists(BaseApplication::class, 'addCommand')) {
39+
// @phpstan-ignore-next-line staticMethod.notFound - addCommand method exists only on Symfony Console 8.0+
40+
return parent::addCommand($command);
41+
}
42+
43+
// For Symfony Console < 8.0, ensure we only pass Command instances
44+
if (!$command instanceof Command) {
45+
throw new \InvalidArgumentException('Command must be an instance of '.Command::class.' for Symfony Console < 8.0');
46+
}
47+
3448
return parent::add($command);
3549
}
3650
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace FriendsOfTwig\Twigcs\Tests\Unit\Console;
6+
7+
use FriendsOfTwig\Twigcs\Console\Application;
8+
use FriendsOfTwig\Twigcs\Console\ContainerAwareCommand;
9+
use FriendsOfTwig\Twigcs\Console\LintCommand;
10+
use FriendsOfTwig\Twigcs\Console\RegDebugCommand;
11+
use FriendsOfTwig\Twigcs\Container;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\Console\Application as BaseApplication;
14+
use Symfony\Component\Console\Command\Command;
15+
16+
/**
17+
* @internal
18+
*
19+
* @covers \FriendsOfTwig\Twigcs\Console\Application
20+
*/
21+
final class ApplicationTest extends TestCase
22+
{
23+
public function testAddCommandWithRegularCommand(): void
24+
{
25+
$application = new Application(false);
26+
$command = new Command('test:command');
27+
28+
$result = $application->addCommand($command);
29+
30+
self::assertSame($command, $result);
31+
self::assertTrue($application->has('test:command'));
32+
}
33+
34+
public function testAddCommandWithContainerAwareCommand(): void
35+
{
36+
$application = new Application(false);
37+
$command = new RegDebugCommand();
38+
39+
$result = $application->addCommand($command);
40+
41+
self::assertSame($command, $result);
42+
self::assertNotNull($command->getContainer());
43+
self::assertTrue($application->has('reg:debug'));
44+
}
45+
46+
public function testAddCommandWithLintCommand(): void
47+
{
48+
$application = new Application(false);
49+
$command = new LintCommand();
50+
51+
$result = $application->addCommand($command);
52+
53+
self::assertSame($command, $result);
54+
self::assertNotNull($command->getContainer());
55+
self::assertTrue($application->has('lint'));
56+
}
57+
58+
public function testAddCommandReplacesExistingCommandWithSameName(): void
59+
{
60+
$application = new Application(false);
61+
$command1 = new Command('test:command');
62+
$command2 = new Command('test:command');
63+
64+
$result1 = $application->addCommand($command1);
65+
$result2 = $application->addCommand($command2);
66+
67+
// The first command should be returned
68+
self::assertSame($command1, $result1);
69+
// The second command should replace the first one
70+
self::assertSame($command2, $result2);
71+
// The application should have the second command registered
72+
self::assertSame($command2, $application->get('test:command'));
73+
}
74+
75+
public function testAddCommandSetsContainerForContainerAwareCommand(): void
76+
{
77+
$application = new Application(false);
78+
$command = new RegDebugCommand();
79+
80+
// Before adding, container should be null
81+
self::assertNull($command->getContainer());
82+
83+
$application->addCommand($command);
84+
85+
// After adding, container should be set
86+
self::assertNotNull($command->getContainer());
87+
}
88+
89+
public function testAddCommandDoesNotSetContainerForRegularCommand(): void
90+
{
91+
$application = new Application(false);
92+
$command = new Command('regular:command');
93+
94+
// Regular commands don't have getContainer method, so we just verify it doesn't crash
95+
$result = $application->addCommand($command);
96+
97+
self::assertSame($command, $result);
98+
self::assertTrue($application->has('regular:command'));
99+
}
100+
101+
public function testAddCommandWithSymfony80Plus(): void
102+
{
103+
// Skip this test if we're not on Symfony 8.0+
104+
if (!method_exists(BaseApplication::class, 'addCommand')) {
105+
self::markTestSkipped('This test requires Symfony Console 8.0+');
106+
}
107+
108+
$application = new Application(false);
109+
$command = new Command('test:command');
110+
111+
$result = $application->addCommand($command);
112+
113+
self::assertSame($command, $result);
114+
self::assertTrue($application->has('test:command'));
115+
116+
// Test that ContainerAwareCommand gets its container set
117+
$containerAwareCommand = new RegDebugCommand();
118+
self::assertNull($containerAwareCommand->getContainer());
119+
120+
$application->addCommand($containerAwareCommand);
121+
self::assertNotNull($containerAwareCommand->getContainer());
122+
}
123+
124+
public function testAddCommandFallsBackToAddWithSymfonyBefore80(): void
125+
{
126+
// Skip this test if we're on Symfony 8.0+
127+
if (method_exists(BaseApplication::class, 'addCommand')) {
128+
self::markTestSkipped('This test requires Symfony Console < 8.0');
129+
}
130+
131+
$application = new Application(false);
132+
$command = new Command('test:command');
133+
134+
$result = $application->addCommand($command);
135+
136+
self::assertSame($command, $result);
137+
self::assertTrue($application->has('test:command'));
138+
139+
// Test that ContainerAwareCommand gets its container set
140+
$containerAwareCommand = new RegDebugCommand();
141+
self::assertNull($containerAwareCommand->getContainer());
142+
143+
$application->addCommand($containerAwareCommand);
144+
self::assertNotNull($containerAwareCommand->getContainer());
145+
146+
// Test that callable throws exception on old versions
147+
$callable = function () {
148+
return new Command('callable:command');
149+
};
150+
151+
$this->expectException(\InvalidArgumentException::class);
152+
$this->expectExceptionMessage('Command must be an instance of');
153+
$application->addCommand($callable);
154+
}
155+
}

0 commit comments

Comments
 (0)