Reputation: 977
I'm currently creating a php artisan console command in a Laravel 5.1 project, and want to call another console command from my console command. This third party command I want to call does not accept any options or arguments, but rather receives its input via interactive questions.
I know I can call a command with options and arguments like this:
$this->call('command:name', ['argument' => 'foo', '--option' => 'bar']);
I also know I can call an interactive command without interactions like this from the command line:
php artisan command:name --no-interaction
But how can I answer these interactive questions from within my command?
I would like to do something like the below (pseudo code).
$this->call('command:name', [
'argument' => 'foo',
'--option' => 'bar'
], function($console) {
$console->writeln('Yes'); //answer an interactive question
$console-writeln('No'); //answer an interactive question
$console->writeln(''); //skip answering an interactive question
} );
Of course the above doesn't work, since $this->call($command, $arguments)
does not accept a third callback parameter.
How can I answer interactive questions when calling a console command from a console command?
Upvotes: 7
Views: 7767
Reputation: 5754
With mpyw/streamable-console: Call interactive artisan command using arbitrary stream:
$this->usingInputStream("yes\nno\n")->call('command:name');
Upvotes: 1
Reputation: 1999
I have another solution, it is to call a symfony command executing 'php artisan' instead of using artisan sub-commands. I think that's better than patching 3rd party code.
Here is a trait which manages this.
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
trait ArtisanCommandTrait{
public function executeArtisanCommand($command, $options){
$stmt = 'php artisan '. $command . ' ' . $this->prepareOptions($options);
$process = new Process($stmt);
$process->run();
// executes after the command finishes
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process->getOutput();
}
public function prepareOptions($options){
$args = [];
$opts = [];
$flags = [];
foreach ($options as $key => $value) {
if(ctype_alpha(substr($key, 0, 1)))
$args[] = $value;
else if(starts_with($key, '--')){
$opts[] = $key. (is_null($value) ? '' : '=' . $value) ;
}
else if(starts_with($key, '-')){
$flags[] = $key;
}
}
return implode(' ', $args) . ' '
.implode(' ', $opts). ' '
.implode(' ', $flags);
}
}
Now, you should be able to pass any artisan special options such as no-interaction.
public function handle(){
$options = [
'argument' => $argument,
'--option' => $options, // options should be preceded by --
'-n' => null // no-interaction option
];
$command = 'your:command';
$output = $this->executeArtisanCommand($command, $options);
echo $output;
}
You can download the trait from this gist
Upvotes: 4
Reputation: 977
Here's how I did it.
Beware: this patches the core Symfony class QuestionHelper@doAsk
, and although this code runs fine for my purposes (I'm currently just making a proof of concept), this code should probably not run in any production environment.
I'm not accepting my own answer yet, would like to know if there's a better way to do this.
The following assumes a Laravel 5.1 installation.
First composer-require the Patchwork package. I'm using this to augment the functionality of that Symfony class method.
composer require antecedent/patchwork
Edit bootstrap/app.php
and add the following right after the application is created. (Patchwork is not autoloaded)
if($app->runningInConsole()) {
require_once(__DIR__ . '/../vendor/antecedent/patchwork/Patchwork.php');
};
Add the following two use statements to the top of your console command class
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
augment/patch QuestionHelper@doAsk
by using these helper methods on your console command class
public function __construct() {
parent::__construct();
$this->patchAskingQuestion();
}
/**
* Patch QuestionHelper@doAsk
* When a key 'qh-patch-answers' is found in the $_REQUEST superglobal,
* We assume this is an array which holds the answers for our interactive questions.
* shift each answer off the array, before answering the corresponding question.
* When an answer has a NULL value, we will just provide the default answer (= skip question)
*/
private function patchAskingQuestion() {
\Patchwork\replace('Symfony\Component\Console\Helper\QuestionHelper::doAsk', function(OutputInterface $output, Question $question) {
$answers = &$_REQUEST['qh-patch-answers'];
//No predefined answer found? Just call the original method
if(empty($answers)) {
return \Patchwork\callOriginal([$output, $question]);
}
//using the next predefined answer, or the default if the predefined answer was NULL
$answer = array_shift($answers);
return ($answer === null) ? $question->getDefault() : $answer;
});
}
private function setPredefinedAnswers($answers) {
$_REQUEST['qh-patch-answers'] = $answers;
}
private function clearPredefinedAnswers() {
unset($_REQUEST['qh-patch-answers']);
}
You can now answer interactive questions like this
public function fire() {
//predefine the answers to the interactive questions
$this->setPredefinedAnswers([
'Yes', //first question will be answered with 'Yes'
'No', //second question will be answered with 'No'
null, //third question will be skipped (using the default answer)
null, //fourth question will be skipped (using the default answer)
]);
//call the interactive command
$this->call('command:name');
//clean up, so future calls to QuestionHelper@doAsk will definitely call the original method
$this->clearPredefinedAnswers();
}
Upvotes: 1