Yonel Ceruto
Contributed by Yonel Ceruto in #62567

Symfony provides several ways to create console commands. The recommended approach today is to use the #[AsCommand] attribute to register commands directly in your command classes, keeping configuration simple and explicit.

However, when working on real-world applications, it's common to have multiple closely related commands that share the same dependencies (a repository, an API client, a logger). Defining each command in its own class quickly leads to repetitive boilerplate, especially duplicated constructors wiring the same services.

Symfony 8.1 solves this by letting you group related commands inside a single class, applying the #[AsCommand] attribute to individual methods. This mirrors how Symfony already handles multiple controller actions in one class or multiple message handlers in one handler class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;

class UserCommands
{
    public function __construct(
        private UserRepository $users,
        private LoggerInterface $logger,
    ) {
    }

    #[AsCommand('app:user:create', description: 'Creates a new user')]
    public function create(OutputInterface $output): int
    {
        // ...

        return Command::SUCCESS;
    }

    #[AsCommand('app:user:delete', description: 'Deletes an existing user')]
    public function delete(OutputInterface $output): int
    {
        // ...

        return Command::SUCCESS;
    }
}

Each annotated method is registered as an independent command thanks to autoconfiguration, so no extra wiring is needed. The constructor is defined once, and every command in the class reuses the same injected dependencies.

When using the Console component standalone (without the service container), register each method as a first-class callable:

1
2
3
4
5
$instance = new UserCommands($users, $logger);

$application = new Application();
$application->addCommand($instance->create(...));
$application->addCommand($instance->delete(...));

Testing works the same way. Pass the first-class callable to CommandTester to exercise a single method:

1
2
3
$tester = new CommandTester((new UserCommands($users, $logger))->create(...));
$tester->execute([]);
$tester->assertCommandIsSuccessful();
Published in #Living on the edge