Invokable commands and input attributes were one of the most popular and important features introduced in Symfony 7.3. In Symfony 7.4, we've improved them in several ways.
Enum Support in Invokable Commands
The type of the #[Argument] and #[Option] attributes can now be backed
enums. The string input provided by the user is automatically converted into the
corresponding enum. If you define the following enums:
1 2 3 4 5 6 7 8 9 10 11 12 13
enum CloudRegion: string
{
case East = 'east';
case West = 'west';
}
enum ServerSize: int
{
case S = 1;
case M = 2;
case L = 3;
case XL = 4;
}
You can then use them in your command as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#[AsCommand('app:add-server')]
class AddServerCommand
{
public function __invoke(
OutputInterface $output,
#[Argument] CloudRegion $region,
#[Option] ?ServerSize $size = null,
): int {
$output->writeln($region->value);
$output->writeln($size?->value ?? 'No value');
return Command::SUCCESS;
}
}
If the user provides a value that doesn't match the enum, Symfony displays a clear error message showing the possible values:
1 2 3 4 5
$ php bin/console app:add-server east --size=2
$ php bin/console app:add-server east-1
The value "east-1" is not valid for the "region" argument.
Supported values are "east", "west".
New MapInput Attribute
When a command defines many arguments and/or options, its __invoke() method
can look crowded. In Symfony 7.4, we're introducing a new #[MapInput] attribute
that can be applied to a DTO class where you define the command arguments and options.
Continuing the previous example, you could define the command input using this DTO (the only requirement is that properties must be public and non-static):
1 2 3 4 5 6 7 8
class AddServerInput
{
#[Argument]
public CloudRegion $region;
#[Option]
public ?ServerSize $size = null;
}
Then you can define the command like this:
1 2 3 4 5 6 7 8 9 10 11 12
#[AsCommand('app:add-server')]
class AddServerCommand
{
public function __invoke(
OutputInterface $output,
#[MapInput] AddServerInput $server,
): int {
// use $server->region and $server->size
return Command::SUCCESS;
}
}
You can even nest DTOs, and Symfony will merge them all to build the full list of arguments and options.
Support Invokable Commands in Tests
The CommandTester class eases testing commands using special input and output classes to test commands without a real console. In Symfony 7.4, we updated it to make sure it's fully compatible with all the new invokable command features.
Interactive Invokable Commands
Symfony commands provide interactive features to ask for missing arguments or
options, retry inputs when invalid values are given, and more. In Symfony 7.4,
we've enhanced invokable commands to include interactive features through two
new attributes: #[Interact] and #[Ask].
First, you can apply the #[Interact] attribute to any non-static public
method in your command. That method will be called during interactive mode.
Previously, you had to define a method called interact():
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#[AsCommand('app:add-server')]
class AddServerCommand extends Command
{
protected function interact(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
if (!$input->getArgument('region')) {
$input->setArgument('region', $io->ask('Enter the cloud region name'));
}
}
// ...
}
Now you can apply the #[Interact] attribute to any method of the command and
use a more convenient method signature:
1 2 3 4 5 6 7 8 9 10 11 12 13
#[AsCommand('app:add-server')]
class AddServerCommand
{
#[Interact]
public function prompt(InputInterface $input, SymfonyStyle $io): void
{
if (!$input->getArgument('region')) {
$input->setArgument('region', $io->ask('Enter the cloud region name'));
}
}
// ...
}
If your interactions only involve asking for missing argument or option values,
you can make them even more concise with the #[Ask] attribute:
1 2 3 4 5 6 7 8 9 10 11
#[AsCommand('app:add-server')]
class AddServerCommand
{
public function __invoke(
#[Argument, Ask('Enter the cloud region name')]
CloudRegion $region,
// ...
): int {
// ...
}
}
The #[Ask] attribute can also be applied to DTOs that define the input:
1 2 3 4 5 6 7 8 9
class AddServerInput
{
#[Argument]
#[Ask('Enter the cloud region name')]
public CloudRegion $region;
#[Option]
public ?ServerSize $size = null;
}
One of my favorite new features of Symfony 7.4. Even the simple example of needing offset and limit options can use a DTO to reduce duplication and visual clutter. The Symfony Console just keeps getting better!
This is amazing! Thank you all for these improvements.
May I ask you why do you use the Command class to reference the constants, rather than self? E.g. why Command::SUCCESS rather than self::SUCCESS?
Edit: I see now that this is not possible because the commands don't extend the base Command class anymore. My bad.
This is lit 🔥🔥, thank you for this.
Excellent news, I’ll share it on my website!! https://mrk-cu.com/blog/novedades-en-symfony-7-4
Making the Console component even more awesome, great!