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.


Yonel Ceruto Robin Chalas
Contributed by Yonel Ceruto and Robin Chalas in #59340 , #59473 , #59493 and #60024

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 returns Command::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.

Published in #Living on the edge