Robin Chalas
Contributed by Robin Chalas in #62917

In recent Symfony versions, console commands have become much more concise and expressive. Thanks to invokable commands and attributes like MapInput, you can now define commands, arguments and options directly in the __invoke() method:

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
29
30
31
// src/Command/GenerateReportCommand.php
namespace App\Command;

use App\Repository\UserRepository;
use App\Service\ReportGenerator;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;

#[AsCommand(name: 'app:report:generate')]
final class GenerateReportCommand
{
    public function __construct(
        private UserRepository $userRepository,
        private ReportGenerator $reportGenerator,
    ) {
    }

    public function __invoke(
        #[Argument] int $userId,
        #[Option] string $dateYmd,
    ): int {
        $user = $this->userRepository->find($userId);
        $date = \DateTimeImmutable::createFromFormat('Y-m-d', $dateYmd);

        // ...

        return Command::SUCCESS;
    }
}

Although this approach is already much cleaner than manually reading values from the input object, commands still need to transform raw CLI values (e.g. a user ID integer) into application objects themselves (e.g. a User Doctrine entity).

Symfony 8.1 improves this with console argument resolvers. If you've used argument resolvers in controllers, this feature will feel instantly familiar. It's the exact same mechanism, but applied to console arguments and options.

For example, instead of manually loading the user and parsing the date, Symfony can now do that automatically for you:

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 App\Entity\User;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\Console\Attribute\MapDateTime;

#[AsCommand(name: 'app:report:generate')]
final class GenerateReportCommand
{
    public function __construct(
        private ReportGenerator $reports,
    ) {
    }

    public function __invoke(
        // resolves the entity by its primary key
        #[Argument, MapEntity]
        User $user,

        // use this instead if the argument contains the user's email
        // #[Argument, MapEntity(mapping: ['user' => 'email'])]
        // User $user,

        #[Option, MapDateTime(format: 'Y-m-d')]
        \DateTimeInterface $date,
    ): int {
        // ...
    }
}

Running the command with php bin/console app:report:generate 42 --date=2026-05-08 now automatically resolves the User entity from the database and converts the option value into a DateTimeInterface object.

Symfony 8.1 includes several built-in resolvers for common use cases, including backend enums and UUID or ULID values. In addition, you can now inject services directly into the __invoke() method too, instead of defining them in the constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Before:
public function __construct(
    private ReportGenerator $reports,
) {
}

public function __invoke(
    #[Argument] int $userId,
    // ...
): int {

// After
public function __invoke(
    ReportGenerator $reports,
    #[Argument] int $userId,
    // ...
): int {

All dependency injection features already available in controllers, including #[Autowire] and #[Target], can be used too:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\Messenger\MessageBusInterface;

#[AsCommand(name: 'app:audit')]
final class AuditCommand
{
    public function __invoke(
        #[Autowire(service: 'messenger.bus.async')] MessageBusInterface $bus,
        #[Target('security')] LoggerInterface $logger,
        #[Autowire('%kernel.environment%')] string $env,
    ): int {
        // ...
    }
}

Like controller resolvers, console resolvers are fully extensible. You can create your own ValueResolverInterface implementations to map command arguments and options to any custom object or value.

Read the Symfony Console documentation to learn more about built-in resolvers, targeted resolvers and custom resolvers.

Published in #Living on the edge