Symfony 8.1 ships many features related to the Console component, such as HTTP-less Symfony applications, method-based commands, and console argument resolvers. In addition, it includes several improvements to console command input handling.
Pasting Images As Answers
Pasting an image directly into a terminal felt like science fiction just a few years ago. It's now a familiar gesture, popularized by AI command-line tools where dropping a screenshot or a picture into the prompt has become routine. Symfony 8.1 brings that same capability to your own console commands.
When a command parameter is typed as InputFile and uses the #[Ask]
attribute, the question helper switches to file input mode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Ask;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\File\InputFile;
#[AsCommand('app:analyze')]
class AnalyzeCommand
{
public function __invoke(
#[Argument, Ask('Provide an image (paste it or enter a path):')]
InputFile $image,
): int {
// $image comes from a pasted image or a file path
}
}
In terminals that support the underlying graphics protocols (Ghostty, iTerm2, Kitty, WezTerm, Konsole, Warp, and more), users can paste an image directly from their clipboard. In other terminals, the prompt gracefully falls back to accepting a file path.
Interactive Choice Questions
Invokable commands can already prompt for missing values with the #[Ask]
attribute. Symfony 8.1 complements this with #[AskChoice], a declarative
way to ask users to select one or more values from a list:
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\AskChoice;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand('app:create-user')]
class CreateUserCommand
{
public function __invoke(
#[Argument, AskChoice('Select a role', ['admin', 'editor', 'viewer'])]
string $role,
): int {
// $role is one of: 'admin', 'editor', 'viewer'
}
}
The attribute adapts to the parameter type: typing it as array enables
multiple selections, and typing it as BackedEnum derives the choices
automatically from the enum cases, so you don't have to repeat them:
1 2 3 4 5 6 7 8 9 10 11 12
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
}
public function __invoke(
#[Argument, AskChoice('Select a status')]
Status $status, // choices: 'active', 'inactive'
): int {
// ...
}
Boolean Defaults for Negatable Options
Negatable options (InputOption::VALUE_NEGATABLE) accept both a flag (e.g.
--yell) and its negation (e.g. --no-yell). Symfony 8.1 lets you define a
boolean default value for them:
1 2 3 4
$this
// ...
->addOption('yell', null, InputOption::VALUE_NEGATABLE, 'Whether to yell', false)
;
Objects As Default Values for Options And Arguments
Invokable commands can already map arguments and options to objects (such as
\DateTimeImmutable). However, you couldn't use an object as the default
value for such an input, which was problematic because options in invokable
commands must define a default value. Symfony 8.1 removes that limitation:
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\AsCommand;
use Symfony\Component\Console\Attribute\Option;
#[AsCommand('app:report')]
class ReportCommand
{
public function __invoke(
#[Argument] string $name,
#[Option] \DateTimeImmutable $from = new \DateTimeImmutable(),
): int {
// ...
}
}
Accessing And Forwarding the Original Input
Some commands need to forward their own input to a child process (for example,
to parallelize work across multiple subprocesses). This was difficult to do
reliably because the parsed input merges default values and doesn't expose the
original command-line tokens. Symfony 8.1 adds RawInputInterface
(implemented by the built-in input classes) with three methods to solve this:
getRawArguments()andgetRawOptions()return only the arguments and options explicitly passed, without default values merged in;unparse()turns parsed options back into their command-line form (such as['--option=value', '--flag']), ready to pass to another process.
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Console\Input\RawInputInterface;
use Symfony\Component\Process\Process;
// inside a command that receives RawInputInterface $input
$options = $input->getRawOptions();
unset($options['main-process-only-option']);
$process = new Process([
\PHP_BINARY, 'bin/console', 'my:command',
...$input->getRawArguments(),
...$input->unparse(array_keys($options)),
]);
Validating Interactive Answers
Answers entered interactively often require validation (for example, a value must
not be empty or must be a valid email address). In Symfony 8.1 you can validate
them with Symfony Validator constraints, both through the #[Ask] attribute and
the QuestionHelper. When validation fails, the user is prompted again:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Ask;
use Symfony\Component\Validator\Constraints as Assert;
public function __invoke(
#[Argument, Ask('Enter your email:', constraints: [
new Assert\NotBlank(), new Assert\Email()
])]
string $email,
): int {
// $email is guaranteed to be a non-empty, valid email
}
The same applies when working directly with Question objects, via the new
setConstraints() method:
1 2 3 4
$question = new Question('Enter a URL:');
$question->setConstraints([new Assert\Url()]);
$url = $io->askQuestion($question);
Validating Mapped Input Objects
The #[MapInput] attribute maps command arguments and options onto a typed
object. Symfony 8.1 validates that object automatically using Validator
constraints, just like #[MapRequestPayload] does for HTTP controllers:
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
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\MapInput;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Validator\Constraints as Assert;
class CreateUserInput
{
#[Argument]
#[Assert\NotBlank]
public string $name;
#[Option]
#[Assert\Email]
public ?string $email = null;
}
#[AsCommand('app:create-user')]
class CreateUserCommand
{
public function __invoke(#[MapInput] CreateUserInput $input): int
{
// $input is already validated
}
}
Validation runs only when the Validator component is installed; otherwise the
constraints are ignored. On validation failure, an
InputValidationFailedException is thrown with the list of violations. You
can also restrict the checks to certain validation groups with
#[MapInput(validationGroups: ['registration'])].