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();
It may be useful for some cases, for me it's just breaking the "S" from SOLID - Single Responsibility Principle.
In 2026 it's not a problem to have many class files, sorted, dedicated, unique.
Awesome! Now AsCommand joins AsTwigFunction and AsEventListener to make service registration at the method level super-easy.
It's kinda funny to see a push for invokable controllers and allowing multiple methods in a command :) At least it make sens to equalize the constraints