This is the first article in a series showcasing the most important new features introduced by Symfony 7.3, which will be released at the end of May 2025.
The Console component is the most popular Symfony package (excluding the pollyfil packages), with more than 900 million downloads and 11,500 open source projects depending on it. It is also one of the oldest packages, with its first version released in October 2011.
A typical command created with the Symfony Console looks like this:
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
// src/Command/CreateUserCommand.php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:create-user')]
class CreateUserCommand extends Command
{
protected function configure(): void
{
$this->addArgument('name', InputArgument::REQUIRED);
$this->addOption('activate', null, InputOption::VALUE_NONE);
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$name = $input->getArgument('name');
$activate = (bool) $input->getOption('activate');
// ...
return Command::SUCCESS;
}
}
This was fine given the PHP features available at the time. However, with all the powerful new features added to PHP in recent years (mostly attributes), we thought we could significantly improve the DX (developer experience).
That's why in Symfony 7.3, you can define the very same command like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Option;
// ...
#[AsCommand(name: 'app:create-user')]
class CreateUserCommand
{
public function __invoke(
SymfonyStyle $io, #[Argument] string $name, #[Option] bool $activate = false,
): int
{
// ...
return Command::SUCCESS;
}
}
The main changes are:
- Your command class no longer needs to extend Symfony's base
Command
class; - You don't need to override the
configure()
method to define command options and arguments; - The values of options and arguments are available directly as variables, without
needing to call
$input->getOption()
or$input->getArgument()
.
The existing #[AsCommand]
attribute was improved so you can also define the
command help there (instead of inside the configure()
method):
1 2 3 4 5 6 7 8 9 10 11 12
#[AsCommand(
name: 'app:create-user',
description: 'Adds new users to the system and optionally activates them',
help: <<<TXT
The <info>%command.name%</info> command adds a new user with the
username passed to it:
<info>php %command.full_name% jane-doe</info>
// ...
TXT
)]
The new #[Argument]
and #[Option]
attributes allow you to define the same
properties as the previous addArgument()
and addOption()
methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// add a description to explain some details about the argument
#[Argument(description: 'The user login or email address')] string $identifier,
// the command argument is called 'activate', but your code uses a different name
#[Argument(name: 'activate')] bool $isActive,
// an argument with a default value (e.g. 3) is optional
#[Argument] int $retries = 3,
// optional argument with NULL default value when it's not passed
#[Argument] ?string $name,
// an argument of type array with a default value of an empty array
#[Argument] array $ports = [],
// same for options (except that they must always define a default value)
#[Option(name: 'idle')] ?int $timeout = null,
#[Option] string $type = 'USER_TYPE',
#[Option(shortcut: 'v')] bool $verbose = false,
#[Option(description: 'User groups')] array $groups = [],
#[Option(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'],
The previous way of defining commands still works, and we don't plan to deprecate it anytime soon. However, we encourage you to adopt this new, modern, and simpler way of creating commands.
Is this made with zenstruck? (I know Kevin is working with Symfony core team and SymfonyCasts more these days, ofcourse). It looks a lot like his zenstruck/console-extra package.
No harm in taking something and including it in Symfony - with all the support and BC promises. It would be a shame if the work was completely redone or without discussing it with him, though.
I like it ^^
😍
@Joris Mak, the zentruck package has been one inspiration to build this feature into the Symfony Console component. But we came up with an even better solution, since it's no longer necessary to extend the Command class.
Looks great. Looking forward to using this.
Jérôme Tamarelle, nice to hear !
Gorgeous feature !! really liked it <3
This was definitely a team effort! and yeah, Kevin brought all his experience. We took inspiration from existing packages, but made sure everything was discussed and aligned before moving forward
Very interesting !
Guys this is fantastic work ! I really like the self sufficient declaration (no more extends Command).
I have a question though (did not look at the code yet) :
Is it still possible to replicate what was previously done by overriding initialize() or interact() methods that were defined in Command class ? If yes, how ? This was done for example to prompt the user to provide values for some arguments.
Georges-King NJOCK-BÔT Good point! You can still use the new __invoke() + input attributes and extend the Command class if you need to override initialize() or interact(). No limitations there.
What about testing the command?
This is great! Looking forward to upgrading. Thanks!