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.
Backed Enum resolving will prove to be quite useful :)
That's what I wanted!!! Thanks! <3
@Loïc, Good news: backed enum are supported by Invokable Commands since Symfony 7.4: https://symfony.com/blog/new-in-symfony-7-4-improved-invokable-commands#enum-support-in-invokable-commands