Pepijn Olivier Pepijn Olivier - 4 months ago 8
PHP Question

How to call an artisan console command with an interaction

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?

Answer

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();
    }