问题助手
QuestionHelper 提供了询问用户更多信息的功能。它包含在默认助手集中,您可以通过调用 getHelper() 来获取它。
1
$helper = $this->getHelper('question');
Question Helper 有一个单独的方法 ask(),它需要一个 InputInterface 实例作为第一个参数,一个 OutputInterface 实例作为第二个参数,以及一个 Question 作为最后一个参数。
注意
作为替代方案,请考虑使用 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;
}
// ... do something here
return Command::SUCCESS;
}
}
在这种情况下,将询问用户“继续执行此操作吗?”。如果用户回答 y
(或任何以 y
开头的单词、表达式,由于默认答案正则表达式,例如 yeti
),它将返回 true
,否则返回 false
,例如 n
。
__construct() 的第二个参数是用户未输入任何有效输入时返回的默认值。如果未提供第二个参数,则假定为 true
。
提示
您可以自定义用于检查答案是否表示“是”的正则表达式,方法是在构造函数的第三个参数中进行设置。例如,要允许任何以 y
或 j
开头的内容,您可以将其设置为
1 2 3 4 5
$question = new ConfirmationQuestion(
'Continue with this action?',
false,
'/^(y|j)/i'
);
正则表达式默认为 /^y/i
。
注意
默认情况下,question helper 使用错误输出 (stderr
) 作为其默认输出。可以通过将 StreamOutput 的实例传递给 ask() 方法来更改此行为。
询问用户获取信息
您还可以提出一个问题,其答案不仅仅是简单的“是/否”。例如,如果您想知道 bundle 名称,您可以将其添加到您的命令中
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);
// ... do something with the bundleName
return Command::SUCCESS;
}
将询问用户“请输入 bundle 的名称”。他们可以键入一些名称,这些名称将由 ask() 方法返回。如果他们将其留空,则返回默认值(此处为 AcmeDemoBundle
)。
让用户从答案列表中选择
如果您有一组用户可以从中选择的预定义答案,则可以使用 ChoiceQuestion,它确保用户只能输入有效的字符串或从预定义列表中选择答案的索引。在下面的示例中,输入 blue
或 1
对于用户来说是相同的选择。默认值设置为 0
,但可以改为设置 red
(可能更明确)
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(
'Please select your favorite color (defaults to red)',
// choices can also be PHP objects that implement __toString() method
['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
return Command::SUCCESS;
}
应默认选择的选项通过构造函数的第三个参数提供。默认值为 null
,这意味着没有默认选项。
选择题同时显示选择值和数字索引,数字索引默认从 0 开始。用户可以键入数字索引或选择值来进行选择。
1 2 3 4 5
Please select your favorite color (defaults to red):
[0] red
[1] blue
[2] yellow
>
提示
要使用自定义索引,请传递一个带有自定义数字键的数组作为选择值。
1 2 3 4
new ChoiceQuestion('Select a room:', [
102 => 'Room Foo',
213 => 'Room Bar',
]);
如果用户输入无效字符串,则会显示错误消息,并要求用户再次提供答案,直到他们输入有效字符串或达到最大尝试次数。最大尝试次数的默认值为 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,yellow
),甚至混合使用字符串和选择的索引(例如 blue,2
)。
如果用户未输入任何内容,则结果将是:您刚刚选择了:red, blue
。
自动完成
您还可以为给定的问题指定一个潜在答案数组。这些答案将在用户键入时自动完成。
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);
// ... do something with the 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');
// This function is called whenever the input changes and new
// suggestions are needed.
$callback = function (string $userInput): array {
// Strip any characters from the last slash to the end of the string
// to keep only the last directory and generate suggestions for it
$inputPath = preg_replace('%(/|^)[^/]*$%', '$1', $userInput);
$inputPath = '' === $inputPath ? '.' : $inputPath;
// CAUTION - this example code allows unrestricted access to the
// entire filesystem. In real applications, restrict the directories
// where files and dirs can be found
$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);
// ... do something with the 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('What is the name of the child?');
$question->setTrimmable(false);
// if the users inputs 'elsa ' it will not be trimmed and you will get 'elsa ' as value
$name = $helper->ask($input, $output, $question);
// ... do something with the name
return Command::SUCCESS;
}
接受多行答案
默认情况下,question helper 在收到换行符时(即,当用户按一次 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('How do you solve world peace?');
$question->setMultiline(true);
$answer = $helper->ask($input, $output, $question);
// ... do something with the answer
return Command::SUCCESS;
}
多行问题在收到传输结束控制字符后停止读取用户输入(Unix 系统上为 Ctrl-D
,Windows 上为 Ctrl-Z
)。
隐藏用户的回答
您还可以提出问题并隐藏响应。这对于密码尤其方便。
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('What is the database password?');
$question->setHidden(true);
$question->setHiddenFallback(false);
$password = $helper->ask($input, $output, $question);
// ... do something with the password
return Command::SUCCESS;
}
警告
当您要求隐藏响应时,Symfony 将使用二进制文件、更改 stty
模式或使用其他技巧来隐藏响应。如果这些都不可用,它将回退并允许响应可见,除非您使用 setHiddenFallback() 将此行为设置为 false
,如上面的示例所示。在这种情况下,将抛出 RuntimeException
。
注意
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;
}
规范化答案
在验证答案之前,您可以“规范化”答案以修复小错误或根据需要进行调整。例如,在前面的示例中,您询问了 bundle 名称。如果用户错误地在名称周围添加了空格,您可以在验证之前修剪名称。为此,请使用 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('Please enter the name of the bundle', 'AcmeDemoBundle');
$question->setNormalizer(function (string $value): string {
// $value can be null here
return $value ? trim($value) : '';
});
$bundleName = $helper->ask($input, $output, $question);
// ... do something with the bundleName
return Command::SUCCESS;
}
警告
首先调用规范化器,并将返回的值用作验证器的输入。如果答案无效,请不要在规范化器中抛出异常,而让验证器处理这些错误。
验证答案
您甚至可以验证答案。例如,在前面的示例中,您询问了 bundle 名称。按照 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('Please enter the name of the bundle', 'AcmeDemoBundle');
$question->setValidator(function (string $answer): string {
if (!is_string($answer) || 'Bundle' !== substr($answer, -6)) {
throw new \RuntimeException(
'The name of the bundle should be suffixed with \'Bundle\''
);
}
return $answer;
});
$question->setMaxAttempts(2);
$bundleName = $helper->ask($input, $output, $question);
// ... do something with the bundleName
return Command::SUCCESS;
}
$validator
是一个处理验证的回调函数。如果有错误,它应该抛出异常。异常消息显示在控制台中,因此最好在其中放入一些有用的信息。如果验证成功,回调函数还应返回用户输入的值。
您可以使用 setMaxAttempts() 方法设置最大询问次数。如果达到此最大次数,它将使用默认值。使用 null
意味着尝试次数是无限的。只要用户提供无效答案,就会一直询问用户,只有当他们的输入有效时才能继续。
提示
您甚至可以使用 Validator 组件,通过使用 createCallable() 方法来验证输入。
1 2 3 4 5 6 7 8 9
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Validation;
$question = new Question('Please enter the name of the bundle', 'AcmeDemoBundle');
$validation = Validation::createCallable(new Regex([
'pattern' => '/^[a-zA-Z]+Bundle$/',
'message' => 'The name of the bundle should be suffixed with \'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
use Symfony\Component\Console\Question\Question;
// ...
public function execute(InputInterface $input, OutputInterface $output): int
{
// ...
$helper = $this->getHelper('question');
$question = new Question('Please enter your password');
$question->setNormalizer(function (?string $value): string {
return $value ?? '';
});
$question->setValidator(function (string $value): string {
if ('' === trim($value)) {
throw new \Exception('The password cannot be empty');
}
return $value;
});
$question->setHidden(true);
$question->setMaxAttempts(20);
$password = $helper->ask($input, $output, $question);
// ... do something with the password
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
use Symfony\Component\Console\Tester\CommandTester;
// ...
public function testExecute(): void
{
// ...
$commandTester = new CommandTester($command);
// Equals to a user inputting "Test" and hitting ENTER
$commandTester->setInputs(['Test']);
// Equals to a user inputting "This", "That" and hitting ENTER
// This can be used for answering two separated questions for instance
$commandTester->setInputs(['This', 'That']);
// For simulating a positive answer to a confirmation question, adding an
// additional input saying "yes" will work
$commandTester->setInputs(['yes']);
$commandTester->execute(['command' => $command->getName()]);
// $this->assertRegExp('/.../', $commandTester->getDisplay());
}
通过调用 setInputs(),您可以模拟控制台在内部对所有用户输入通过 CLI 执行的操作。此方法仅接受一个数组作为参数,对于命令期望的每个输入,该数组都包含一个字符串,表示用户将键入的内容。这样,您可以通过传递适当的输入来测试任何用户交互(甚至是复杂的交互)。
注意
CommandTester 在每次输入后自动模拟用户按下 ENTER
键,无需传递额外的输入。
警告
在 Windows 系统上,Symfony 使用特殊的二进制文件来实现隐藏问题。这意味着这些问题不使用默认的 Input
控制台对象,因此您无法在 Windows 上测试它们。