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

Robin Chalas
Contributed by Robin Chalas in #63293

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

Yonel Ceruto
Contributed by Yonel Ceruto in #62911

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

Jesper Noordsij
Contributed by Jesper Noordsij in #52058

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

Robin Chalas
Contributed by Robin Chalas in #63183

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

Théo FIDRY
Contributed by Théo FIDRY in #57598

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() and getRawOptions() 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

Robin Chalas
Contributed by Robin Chalas in #63359

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

Robin Chalas
Contributed by Robin Chalas in #63714

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'])].

Published in #Living on the edge