Question Helper(提问助手)

3.3 版本
维护中的版本

QuestionHelper 提供的方法用于向用户询问更多信息。它已经包括在默认的助手集中,你可以调用 getHelperSet() 来获取它:

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

提问助手有一个独立的方法 ask(),需要一个 InputInterface 实例作为第一个参数,一个 OutputInterface 实例作为第二个参数, 以及一个 Question 作为最后一个参数:

要求用户确认 

假设你希望在一个action被执行之前先行确认。添加以下代码到你的命令中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...
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)
    {
        $helper = $this->getHelper('question');
        $question = new ConfirmationQuestion('Continue with this action?', false);
 
        if (!$helper->ask($input, $output, $question)) {
            return;
        }
    }
}

本例中,用户会被问到 "Continue with this action?"。如果用户回答 y 它就返回 true,或者 false,如果答案是 n 的话。__construct() 的第二个参数,是当用户不键入任何有效input时,返回的默认值。如果没有提供第二个参数, true 会被取用。

你可以在构造器的第三个参数中自定义一个正则表达式,用于判断答案是否是 "yes"的意思。例如,允许任何以 yj 开头的(input),你可以这样设置:

1
2
3
4
5
$question = new ConfirmationQuestion(
    'Continue with this action?',
    false,
    '/^(y|j)/i'
);

默认的regex是 /^y/i

询问用户信息 

你也可以用超过一个简单的 yes/no 的答案来向用户提问。例如,如果你想要知道bundle的名称,可以把下面代码添加到你的命令中:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Console\Question\Question;
 
// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
 
    $bundle = $helper->ask($input, $output, $question);
}

用户会被问 "Please enter the name of the bundle"。他们可以键入一些会被 ask() 方法返回的名称。如果用户留空,默认值 (此处是AcmeDemoBundle) 会被返回。

让用户从答案列表中选择 

如果你预定义了一组答案让用户从中选择,你可以使用 ChoiceQuestion,它确保用户只能从预定义列表中输入有效字符串:

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)
{
    // ...
    $helper = $this->getHelper('question');
    $question = new ChoiceQuestion(
        'Please select your favorite color (defaults to red)',
        array('red', 'blue', 'yellow'),
        0
    );
    $question->setErrorMessage('Color %s is invalid.');
 
    $color = $helper->ask($input, $output, $question);
    $output->writeln('You have just selected: '.$color);
 
    // ... do something with the color / 进行颜色操作
}

默认被选中的选项由构造器的第三个参数提供。默认是 null,代表没有默认的选项。

如果用户输入了无效字符串,会显示一个错误信息,用户会被要求再一次提供答案,直到他们输入一个有效字符串,或是达到了尝试上限为止。默认的最大尝试次数是 null,代表可以无限次尝试。你可以使用 setErrorMessage() 定义自己的错误信息。

多选 

有时,可以给出多个答案。 ChoiceQuestion 使用逗号分隔的值,提供了此项功能。默认是禁用的,开启它可使用 setMultiselect():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\Console\Question\ChoiceQuestion;
 
// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $helper = $this->getHelper('question');
    $question = new ChoiceQuestion(
        'Please select your favorite colors (defaults to red and blue)',
        array('red', 'blue', 'yellow'),
        '0,1'
    );
    $question->setMultiselect(true);
 
    $colors = $helper->ask($input, $output, $question);
    $output->writeln('You have just selected: ' . implode(', ', $colors));
}

现在,当用户键入 1,2,结果会是: You have just selected: blue, yellow

如果用户不键入任何内容,结果是: You have just selected: red, blue

自动完成 

对于给定的问题,你也可以提供一个潜在答案的数组。它们将根据用户的敲击而自动完成:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Console\Question\Question;
 
// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $bundles = array('AcmeDemoBundle', 'AcmeBlogBundle', 'AcmeStoreBundle');
    $question = new Question('Please enter the name of a bundle', 'FooBundle');
    $question->setAutocompleterValues($bundles);
 
    $name = $helper->ask($input, $output, $question);
}

隐藏用户响应 

你也可以在问问题时隐藏响应。这对密码来说极为方便:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Console\Question\Question;
 
// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $question = new Question('What is the database password?');
    $question->setHidden(true);
    $question->setHiddenFallback(false);
 
    $password = $helper->ask($input, $output, $question);
}

当你提问并隐藏响应时,Symofny将使用一个二进制的change stty mode或是使用另一种手段来隐藏之。如果都不可用,它就回滚为响应可见,除非你像上例那样,使用 setHiddenFallback() 来将此行为设置成 false。本例中,一个 RuntimeException 异常会被抛出。

验证答案 

你甚至可以验证答案。例如,前面例子中你曾询问过bundle名称。根据Symfony的命名约定,它应该被施以 Bundle 后缀,你可以使用 setValidator() 方法来验证它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Console\Question\Question;
 
// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
    $question->setValidator(function ($answer) {
        if ('Bundle' !== substr($answer, -6)) {
            throw new \RuntimeException(
                'The name of the bundle should be suffixed with \'Bundle\''
            );
        }
        return $answer;
    });
    $question->setMaxAttempts(2);
 
    $name = $helper->ask($input, $output, $question);
}

$validator是一个callback,专门处理验证。它在有错误发生时应抛出一个异常。异常信息会被显示在控制台中,所以在里面放入一些有用的信息是一个很好的实践。回调函数在验证通过时,应该返回用户的input。

你可以用 setMaxAttempts() 方法来设置(验证失败时的)最大的提问次数。如果达到最大值,它将使用默认值。使用 null 代表可以无限次尝试回答(直到验证通过)。用户将被始终提问,直到他们提供了有效答案为止,也只有输入有效时命令才会继续执行。

验证一个隐藏的响应 

你也可以在隐藏(答案输入)的提问中使用validator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Symfony\Component\Console\Question\Question;
 
// ...
public function execute(InputInterface $input, OutputInterface $output)
{
    // ...
    $helper = $this->getHelper('question');
 
    $question = new Question('Please enter your password');
    $question->setValidator(function ($value) {
        if (trim($value) == '') {
            throw new \Exception('The password can not be empty');
        }
 
        return $value;
    });
    $question->setHidden(true);
    $question->setMaxAttempts(20);
 
    $password = $helper->ask($input, $output, $question);
}

测试一个对输入有所预期的命令 

如果你需要对一个命令写单元测试,而此命令又预期接到从命令行中得到的某些类型的输入,你需要设置helper input stream:

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\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Tester\CommandTester;
 
// ...
public function testExecute()
{
    // ...
    $commandTester = new CommandTester($command);
 
    $helper = $command->getHelper('question');
    $helper->setInputStream($this->getInputStream("Test\n"));
    // Equals to a user inputting "Test" and hitting ENTER
    // If you need to enter a confirmation, "yes\n" will work
    // 这等同于用户输入 “Test” 并敲击ENTER
    // 若你需要在输入时确认,可以使用 "yes\n"
 
    $commandTester->execute(array('command' => $command->getName()));
 
    // $this->assertRegExp('/.../', $commandTester->getDisplay());
}
 
protected function getInputStream($input)
{
    $stream = fopen('php://memory', 'r+', false);
    fputs($stream, $input);
    rewind($stream);
 
    return $stream;
}

通过设置 QuestionHelper 的输入流(input stream),你把用户通过CLI完成的输入,用于模拟命令行的内部运作。通过传入一个合适的input stream,这种方式可以测试任何的用户操作(甚至很复杂的)。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)