diff --git a/console/input.rst b/console/input.rst index c3299e3ab78..970926b7438 100644 --- a/console/input.rst +++ b/console/input.rst @@ -597,6 +597,224 @@ they are assigned to the DTO:: With this setup, when the command input is resolved, the email is lowercased and trimmed, and roles are uppercased. +.. _console-interactive-input: + +Interactive Input +----------------- + +.. versionadded:: 7.4 + + The ``#[Ask]`` and ``#[Interact]`` attributes were introduced in Symfony 7.4. + +In :ref:`invokable commands `, you can use the +:class:`Symfony\\Component\\Console\\Attribute\\Ask` attribute to prompt +users for missing values during the interactive phase, without writing a +custom ``interact()`` method. + +Asking for Simple Values +~~~~~~~~~~~~~~~~~~~~~~~~ + +Add the ``#[Ask]`` attribute to an ``__invoke()`` parameter alongside +``#[Argument]``:: + + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Attribute\Ask; + use Symfony\Component\Console\Command\Command; + + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand + { + public function __invoke( + #[Argument] + #[Ask('Enter the username')] + string $username, + ): int { + // ... + + return Command::SUCCESS; + } + } + +When the ``username`` argument is not provided, the user is prompted: + +.. code-block:: terminal + + $ php bin/console app:create-user + + Enter the username: + > johndoe + +The ``#[Ask]`` attribute supports the following options: + +=============== ================================ ============ ============================================= +Option Type Default Description +=============== ================================ ============ ============================================= +``question`` ``string`` *(required)* The prompt text +``default`` ``string|bool|int|float|null`` ``null`` Default value if user presses Enter +``hidden`` ``bool`` ``false`` Hide input (for passwords) +``multiline`` ``bool`` ``false`` Allow newlines in the response +``trimmable`` ``bool`` ``true`` Trim whitespace from the response +``timeout`` ``int|null`` ``null`` Max seconds to wait for an answer +``normalizer`` ``callable|null`` ``null`` Normalize the input before validation +``validator`` ``callable|null`` ``null`` Custom validator +``maxAttempts`` ``int|null`` ``null`` Max retry attempts (``null`` = unlimited) +=============== ================================ ============ ============================================= + +Asking for Hidden Input +~~~~~~~~~~~~~~~~~~~~~~~ + +Use the ``hidden`` option to mask the user's input (e.g. for passwords):: + + public function __invoke( + #[Argument] + #[Ask('Enter the password', hidden: true)] + string $password, + ): int { + // ... + } + +Asking for Confirmation +~~~~~~~~~~~~~~~~~~~~~~~ + +When the parameter type is ``bool``, the ``#[Ask]`` attribute automatically +uses a confirmation question:: + + public function __invoke( + #[Argument] + #[Ask('Do you want to activate the user?')] + bool $active, + ): int { + // ... + } + +.. note:: + + Interactive attributes (``#[Ask]``, ``#[Interact]``) can only be used + with required console arguments. Using them with options or optional + arguments is not supported and will raise an exception. + +Using ``#[Ask]`` on DTO Properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``#[Ask]`` attribute also works on properties of +:ref:`input DTOs `:: + + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\Ask; + + class CreateUserInput + { + #[Argument] + #[Ask('Enter the username')] + public string $username; + + #[Argument] + #[Ask('Enter the password', hidden: true)] + public string $password; + } + +Then use it in your command with ``#[MapInput]``:: + + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Attribute\MapInput; + use Symfony\Component\Console\Command\Command; + + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand + { + public function __invoke(#[MapInput] CreateUserInput $input): int + { + // use $input->username and $input->password + + return Command::SUCCESS; + } + } + +.. _console-interact-attribute: + +Custom Interactive Logic with ``#[Interact]`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For scenarios that go beyond simple prompts, use the +:class:`Symfony\\Component\\Console\\Attribute\\Interact` attribute to mark +a method that will be called during the interactive phase:: + + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\AsCommand; + use Symfony\Component\Console\Attribute\Ask; + use Symfony\Component\Console\Attribute\Interact; + use Symfony\Component\Console\Command\Command; + use Symfony\Component\Console\Style\SymfonyStyle; + + #[AsCommand(name: 'app:create-user')] + class CreateUserCommand + { + #[Interact] + public function prompt(SymfonyStyle $io): void + { + // custom interactive logic + } + + public function __invoke( + #[Argument] + #[Ask('Enter the username')] + string $username, + ): int { + // ... + + return Command::SUCCESS; + } + } + +The method marked with ``#[Interact]`` must be public and non-static. It +supports the same dependency-injected parameters as ``__invoke()`` (e.g. +``SymfonyStyle``, ``InputInterface``). + +You can also use ``#[Interact]`` on a DTO class to handle interaction +logic related to the DTO's own properties:: + + use Symfony\Component\Console\Attribute\Argument; + use Symfony\Component\Console\Attribute\Ask; + use Symfony\Component\Console\Attribute\Interact; + use Symfony\Component\Console\Style\SymfonyStyle; + + class CreateUserInput + { + #[Argument] + #[Ask('Enter the username')] + public string $username; + + #[Argument] + #[Ask('Enter the password (or press Enter for a random one)', hidden: true)] + public string $password; + + #[Interact] + public function prompt(SymfonyStyle $io): void + { + if (!isset($this->password)) { + $this->password = bin2hex(random_bytes(10)); + $io->writeln('Password generated: '.$this->password); + } + } + } + +Execution Order +~~~~~~~~~~~~~~~ + +When both ``#[Ask]`` and ``#[Interact]`` are used, they are executed in the +following order during the interactive phase: + +#. ``#[Ask]`` on ``__invoke()`` parameters +#. ``#[Ask]`` on DTO properties +#. ``#[Interact]`` on the DTO class +#. ``#[Interact]`` on the command class + +.. note:: + + Interactive prompts only run when the command is executed in interactive + mode. They are skipped when using the ``--no-interaction`` (``-n``) option. + Options with optional arguments ------------------------------- diff --git a/reference/attributes.rst b/reference/attributes.rst index c77f54b5869..dc498a5d3e6 100644 --- a/reference/attributes.rst +++ b/reference/attributes.rst @@ -15,7 +15,9 @@ Command ~~~~~~~ * :ref:`Argument ` +* :ref:`Ask ` * :ref:`AsCommand ` +* :ref:`Interact ` * :ref:`MapInput ` * :ref:`Option `