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
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,
): void
{
// ...
}
}
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
__invoke()
method returnsCommand::SUCCESS
by default, so you don't need to add that return value explicitly.
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
#[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.