Помічник Question
Дата оновлення перекладу 2024-04-30
Помічник Question
QuestionHelper надає функції для того, щоб запитати у користувача більше інформації. Його включено до набору помічників за замовчуванням, який ви можете побачити, викликавши getHelperSet():
1
$helper = $this->getHelper('question');
Помічник Question має єдиний метод ask(), якому потрібен екземпляр InputInterface в якості першого аргументу, екземпляр OutputInterface - в якості другого, і Question в якості останнього аргументу.
Note
В якості альтернативи розгляньте використання SymfonyStyle , щоб ставити запитання.
Запит підтвердження від користувача
Припустимо, що ви хочете підтвердити дію перед тим, як її виконувати. Додайте у вашу команду наступне:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class YourCommand extends Command
{
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Continue with this action?', false);
if (!$helper->ask($input, $output, $question)) {
return Command::SUCCESS;
}
// ... зробіть щось тут
return Command::SUCCESS;
}
}
У цьому випадку, користувача запитають "Ви хочете продовжити цю дію?". Якщо користувач
відповість y
, то повернеться true
, а false
повернеться, якщо відповідь буде
n
.
Другий аргумент методу
__construct()
- це значення за замовчуванням, яке треба повернути, якщо користувач введе невалідне
значення введення. Якщо другий аргумент не надано, то припускається true
.
Tip
Ви можете налаштувати використовуваний регулярний вираз так, щоб перевіряти, чи
означає відповідь "yes", у третьому аргументі конструктора. Наприклад, щоб дозволити
все, що починається з y
або j
, вам потрібно встановити його так:
1 2 3 4 5
$question = new ConfirmationQuestion(
'Продовжити цю дію?',
false,
'/^(y|j)/i'
);
Регулярний вираз за замовчуванням - /^y/i
.
Note
За замовчуванням помічник question використовує виведення помилок (stderr
).
Цю поведінку можна змінити, передавши екземпляр
StreamOutput до
методу ask().
Запит інформації у користувача
Ви також можете поставити питання зі складнішою відповіддю, ніж так/ні. Наприклад, якщо ви хочете дізнатися імʼя пакету, ви можете додати у вашу команду наступне:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
$bundleName = $helper->ask($input, $output, $question);
// ... зробити щось з bundleName
return Command::SUCCESS;
}
Користувача попросять "Будь ласка, введіть імʼя пакету". Він може вивести якесь імʼя,
яке буде повернено методом ask().
Якщо він залишить поле порожнім, то буде повернено значення за замовчуванням (тут - AcmeDemoBundle
).
Дозвольте користувачу обирати зі списку відповідей
Якщо у вас є передвизначений набір відповідей, з якого користувач може обирати, ви можете використати ChoiceQuestion, який гарантує, що користувач може вводити лише валідний рядок з передвизначеного списку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
use Symfony\Component\Console\Question\ChoiceQuestion;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new ChoiceQuestion(
'Будь ласка, оберіть ваш улюблений колір (за замовчуванням - червоний)',
// вибори також можуть бути PHP-обʼєктами, що реалізують метод __toString()
['red', 'blue', 'yellow'],
0
);
$question->setErrorMessage('Color %s is invalid.');
$color = $helper->ask($input, $output, $question);
$output->writeln('You have just selected: '.$color);
// ... зробити щось з кольором something with the color
return Command::SUCCESS;
}
Опція, обрана за замовчуванням, надається третім аргументом конструктора. За
замовчуванням вона null
, що означає, що опції за замовчуванням немає.
Якщо користувачь вводить невалідний рядок, відображається повідомлення про помилку
і користувача попросять надати відповідь ще раз, до тих пір, поки він не введе
валідний рядок або не досягне максимальної кількості спроб. Значення за замовчуванням
для максимальної кількості спроб - null
, що означає нескінченну кількість спроб.
Ви можете визначити ваше власне повідомлення про помилку, використовуючи
setErrorMessage().
Множинний вибір
Іноди можна надати декілька відповідей. ChoiceQuestion
пропонує таку функцію,
використовуючи значення, розділені комами. За замовчуванням це відключено, щоб
підключити, використайте setMultiselect():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
use Symfony\Component\Console\Question\ChoiceQuestion;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new ChoiceQuestion(
'Please select your favorite colors (defaults to red and blue)',
['red', 'blue', 'yellow'],
'0,1'
);
$question->setMultiselect(true);
$colors = $helper->ask($input, $output, $question);
$output->writeln('You have just selected: ' . implode(', ', $colors));
return Command::SUCCESS;
}
Тепер, коли користувач вводить 1,2
, результатом буде:
Ви щойно обрали: синій, жовтий
. Користувач також може вводити рядки
(наприклад, blue,yellow
) і навіть змішувати рядки та індекс варіантів
(наприклад, blue,2
).
Якщо користувач не введе нічого, то результат буде:
Ви щойно обрали: червоний, синій
.
Автозаповнення
Ви також можете вказати масив потенційних відповідей для поставлених запитань. Вони будуть автозаповнені під час того, як користувач друкуватиме:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$bundles = ['AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle'];
$question = new Question('Please enter the name of a bundle', 'FooBundle');
$question->setAutocompleterValues($bundles);
$bundleName = $helper->ask($input, $output, $question);
// ... зробити щось з bundleName
return Command::SUCCESS;
}
У складніших випадках застосування, може бути необхідно генерувати пропозиції на ходу, наприклад, якщо ви хочете автозаповнювати шлях файлу. В такому випадку, ви можете надати функцію зворотного виклику, щоб динамічно генерувати пропозиції:
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 32 33 34
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
// Ця функція викликається кожний раз, коли змінюється введення, і необхідні
// нові пропозиції.
$callback = function (string $userInput): array {
// Приберіть всі символи після останнього слешу до кінця рядку, щоб залишити
// лише останній каталог та згенерувати пропозиції для нього
$inputPath = preg_replace('%(/|^)[^/]*$%', '$1', $userInput);
$inputPath = '' === $inputPath ? '.' : $inputPath;
// УВАГА - цей приклад коду дозволяє необмежений доступ до всієї файлової
// системи. У реальному додатку, обмежте каталоги, де можуть знаходитися
// файли та dir
$foundFilesAndDirs = @scandir($inputPath) ?: [];
return array_map(function (string $dirOrFile) use ($inputPath): string {
return $inputPath.$dirOrFile;
}, $foundFilesAndDirs);
};
$question = new Question('Please provide the full path of a file to parse');
$question->setAutocompleterCallback($callback);
$filePath = $helper->ask($input, $output, $question);
// ... зробити щось з filePath
return Command::SUCCESS;
}
Не усікати відповідь
Ви також можете вказати, що ви хочете не усікати відповідь, встановивши це напряму за допомогою setTrimmable():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Як звуть дитину?');
$question->setTrimmable(false);
// якщо користувач вводить 'elsa ', це не буде обрізано і ви отримаєте 'elsa ' як значення
$name = $helper->ask($input, $output, $question);
// ... зробити щось з іменем
return Command::SUCCESS;
}
Прийняття віповідей у декілька рядків
За замовчуванням, помічник question перестає читати введення користувача, коли він
отримує символ нового рядку (тобто, коли користувач разово натискає ENTER
). Однак,
ви можете вказати, що відповідь на запитання повинна дозволяти відповідь у декілька
рядків, передавши true
в setMultiline():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Як вирішити слово "мир"?');
$question->setMultiline(true);
$answer = $helper->ask($input, $output, $question);
// ... зробити щось з відповіддю
return Command::SUCCESS;
}
Запитання у багато рядків перестають читати введення користувача після отримання
символу контролю кінцю передачі (Ctrl-D
на системах Unix або Ctrl-Z
на Windows).
Приховування відповідей користувача
Ви також можете ставити запитання та приховувати відповідь. Це особливо корисно для паролів:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Який пароль бази даних?');
$question->setHidden(true);
$question->setHiddenFallback(false);
$password = $helper->ask($input, $output, $question);
// ... зробити щось з паролем
return Command::SUCCESS;
}
Caution
Коли ви запитуєте приховану відповідь, Symfony використовуватиме або бінарний режим зміни
stty
, або інший фокус для приховування відповіді. Якщо нічого не доступно, то буде
використано резеврний план і відповідь буде видима, окрім випадків, якщо ви встановите цю
поведінку як false
, використовуючи setHiddenFallback(),
як у прикладі вище. В такому випадку, буде викликано RuntimeException
.
Note
Команда stty
використовується для отримання та утановки властивостей командного
рядку (на кшталт отримання кількості рядків та стовпців або приховування тексту введення).
У системах Windows, команда stty
може генерувати тарабарське виведення та перекручувати
текст введення. Якщо це ваш випадок, відключіть це за допомогою даної команди:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Question\ChoiceQuestion;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
QuestionHelper::disableStty();
// ...
return Command::SUCCESS;
}
Нормалізація відповіді
Перед валідацією відповіді ви можете "нормалізувати" її, щоб виправити дрібні помилки або підлаштувати її за необхідності. Наприклад, у попередньому прикладі ви запитували імʼя пакету. У випадку, якщо користувач додає пробіли навколо імені помилково, ви можете обрізати імʼя перед його валідацією. Щоб зробити це, сконфігуруйте нормалізатор, використовуючи метод setNormalizer():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Будь ласка, введіть імʼя пакету', 'AcmeDemoBundle');
$question->setNormalizer(function (string $value): string {
// $value can be null here
return $value ? trim($value) : '';
});
$bundleName = $helper->ask($input, $output, $question);
// ... зробити щось з bundleName
return Command::SUCCESS;
}
Caution
Спочатку викликається нормалізатор, а повернене значення використовується в якості введення валідатора. Якщо відповідь невалідна, не викликайте виключень у нормалізаторі та дозвольте валідатору обробити ці помилки.
Валідація відповіді
Ви можете навіть валідувати відповідь. Наприклад, у попередньому прикладі ви запитували
імʼя пакету. Дотримуючись угоди іменування Symfony, воно повинно мати суфікс Bundle
.
Ви можете валідувати це, використовуючи метод
setValidator():
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
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Будь ласка, введіть імʼя пакету', 'AcmeDemoBundle');
$question->setValidator(function (string $answer): string {
if (!is_string($answer) || 'Bundle' !== substr($answer, -6)) {
throw new \RuntimeException(
'Імʼя пакету повинно мати суфікс \'Bundle\''
);
}
return $answer;
});
$question->setMaxAttempts(2);
$bundleName = $helper->ask($input, $output, $question);
// ... зробити щось з bundleName
return Command::SUCCESS;
}
$validator
- це зворотний виклик, який працює з валідацією. Він повинен викликати
виключення, якщо щось пішло не так. Повідомлення про виключення відображається у консолі,
тому гарною практикою буде розмістити у ньому корисну інформацію. Функція зворотного
виклику повинна також повертати значення введення користувача, якщо валідація була успішною.
Ви можете встановити максимальну кількість повторень запитання за допомогою методу
setMaxAttempts(). Якщо ви
досягнете максимальної кількості, то буде використано значення за замовчуванням.
Використання null
означає нескінченну кількість спроб. Користувача запитуватимуть
до тих пір, поки він не надасть валідну відповідь, і лише тоді він зможе продовжити.
Tip
Ви навіть можете використати компонент Валідатор, щоб валідувати введення, використовуючи метод createCallable():
1 2 3 4 5 6 7 8 9
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Validation;
$question = new Question('Будь ласка, введіть імʼя пакету', 'AcmeDemoBundle');
$validation = Validation::createCallable(new Regex([
'pattern' => '/^[a-zA-Z]+Bundle$/',
'message' => 'Імʼя пакету повинно мати суфікс \'Bundle\'',
]));
$question->setValidator($validation);
Валідація прихованих відповідей
Ви також можете використати валідатор з прихованим запитанням:
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
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Будь ласка, введіть ваш пароль');
$question->setNormalizer(function (?string $value): string {
return $value ?? '';
});
$question->setValidator(function (string $value): string {
if ('' === trim($value)) {
throw new \Exception('Пароль не може бути порожнім');
}
return $value;
});
$question->setHidden(true);
$question->setMaxAttempts(20);
$password = $helper->ask($input, $output, $question);
// ... зробити щось з паролем
return Command::SUCCESS;
}
Тестування команди, що очікує на введення
Якщо ви хочете написати модульний тест для команди, що очікує на якесь введення з командного рядку, то вам потрібно встановити введення, якого очікуватиме команда:
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\Tester\CommandTester;
// ...
public function testExecute(): void
{
// ...
$commandTester = new CommandTester($command);
// Еквівалентно введенню користувачем "Test" і натисканню ENTER
$commandTester->setInputs(['Test']);
// Еквівалентно введенню користувачем "This", "That" і натисканню ENTER
// Може бути використано для відповіді на два різних запитання, наприклад
$commandTester->setInputs(['This', 'That']);
// Для симуляції позитивної відповіді на запитання підтвердження, буде працювати
// додаткове введення "yes"
$commandTester->setInputs(['yes']);
$commandTester->execute(array('command' => $command->getName()));
$commandTester->execute(['command' => $command->getName()]);
// $this->assertRegExp('/.../', $commandTester->getDisplay());
}
Викликавшши setInputs(), ви імітуєте те, що консоль робитиме внутрішньо з усім введенням користувача через CLI. Цей метод бере масив в якості єдиного аргументу для кожного очікуваного командою введення, разом із рядком, що представляє те, що надрукує користувач. Таким чином, ви можете тестувати будь-яку взаємодію користувача (навіть складні), передаючи відповідні введення.
Note
Клас CommandTester автоматично симулює
натискання користувачем ENTER
після кожного введення, необхідності передавати
додаткове введення немає.
Caution
У системах Windows Symfony використовує спеціальну бінарність, щоб реалізувати
приховані запитання. Це означає, що такі запитання не використовують обʼєкт консолі
за замовчуванням Input
, а отже, ви не зможете тестувати його у Windows.