Помічник Question

Дата оновлення перекладу 2022-12-12

Помічник Question

QuestionHelper надає функції для того, щоб запитати у користувача більше інформації. Його включено до набору помічників за замовчуванням, який ви можете побачити, викликавши getHelperSet():

1
$helper = $this->getHelper('question');

Помічник Question має єдиний метод ask(), якому потрібен екземпляр InputInterface в якості першого аргументу, екземпляр OutputInterface - в якості другого, і Question в якості останнього аргументу.

Запит підтвердження від користувача

Припустимо, що ви хочете підтвердити дію перед тим, як її виконувати. Додайте у вашу команду наступне:

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.

Запит інформації у користувача

Ви також можете поставити питання зі складнішою відповіддю, ніж так/ні. Наприклад, якщо ви хочете дізнатися імʼя пакету, ви можете додати у вашу команду наступне:

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, результатом буде: Ви щойно обрали: синій, жовтий.

Якщо користувач не введе нічого, то результат буде: Ви щойно обрали: червоний, синій.

Автозаповнення

Ви також можете вказати масив потенційних відповідей для поставлених запитань. Вони будуть автозаповнені під час того, як користувач друкуватиме:

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('Будь ласка, введіть імʼя пакету', '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 ($dirOrFile) use ($inputPath) {
            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 ($value) {
        // $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 ($answer) {
        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 ($value) {
        return $value ?? '';
    });
    $question->setValidator(function ($value) {
        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()
{
    // ...
    $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.