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?
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
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();
}
With mpyw/streamable-console: Call interactive artisan command using arbitrary stream:
$this->usingInputStream("yes\nno\n")->call('command:name');
Related
I'm trying to mock a call to the Twilio Rest API for a test I'm writing. This is the code I've written to mock:
$message = $twilioTest->getMockedMessageInstance(['body' => 'This won\'t exist']);
$twilioStub = $this->getMockBuilder(Twilio::class)->getMock();
$twilioStub->expects($this->once())->method('retrieveLastTextFromBot')->willReturn($message);
And the contents of the retrieveLastTextFromBot method are:
$messages = $this->twilioClient->messages->read([
'to' => TwilioDefinitions::getToNumber(),
'from' => getenv('TWILIO_NUMBER'),
], 1);
if (count($messages) !== 1) {
throw new NoExtantTextException('No previous message from ' . getenv('TWILIO_NUMBER') . ' to ' . TwilioDefinitions::getToNumber());
}
return $messages[0];
But obviously, I don't want the contents of the retrieveLastTextFromBot method to execute, that's why I'm mocking it. For some reasons though, that method is executing, and I know that because in my phpunit failure I'm getting this error:
1) CronControllerTest::testRemindMethodErrorHandling
Twilio\Exceptions\RestException: [HTTP 403] Unable to fetch page: Resource not accessible with Test Account Credentials
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Page.php:58
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Page.php:34
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessagePage.php:23
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:147
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:96
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:118
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/app/YmcaScheduler/Utility/Twilio.php:33
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/app/YmcaScheduler/Controller/CronController.php:29
/Users/adammcgurk/Desktop/ymca-scheduler-brains/tests/CronControllerTest.php:22
And line 33 in Twilio.php is that fourth line of the retrieveLastTextFromBot() method, so it's executing.
And maybe I just understand mocking incorrectly, but what I thought was going to happen was the method wouldn't be executed at all, instead, phpunit would just force data to be returned from it.
How can I mock a method in phpunit without it actually being executed?
Working example
I start this answer with a working example. There are two things you can check as explained in this answer. Please update your question with extra information/ details when parts of the answer fit your situation.
Add this code to tests/TwilioTest.php:
<?php
use PHPUnit\Framework\TestCase;
use Twilio\Rest\Client;
class Twilio
{
private $twilioClient;
public function __construct(Client $twilioClient)
{
$this->twilioClient = $twilioClient;
}
public function retrieveLastTextFromBot()
{
$messages = $this->twilioClient->messages->read([
'to' => TwilioDefinitions::getToNumber(),
'from' => getenv('TWILIO_NUMBER'),
], 1);
if (count($messages) !== 1) {
throw new NoExtantTextException('No previous message from ' . getenv('TWILIO_NUMBER') . ' to ' . TwilioDefinitions::getToNumber());
}
return $messages[0];
}
}
class Sut
{
private $twilio;
public function __construct(Twilio $twilio)
{
$this->twilio = $twilio;
}
public function method()
{
$this->twilio->retrieveLastTextFromBot();
}
}
final class TwilioTest extends TestCase
{
//I created a plain php object for the message just for the example
private function getMockedMessageInstance(array $message)
{
return (object) $message;
}
public function test_sut_method(): void
{
$message = $this->getMockedMessageInstance(['body' => 'This won\'t exist']);
$twilioStub = $this
->getMockBuilder(Twilio::class)
->disableOriginalConstructor() # I have added this to ignore the Twilio constructor dependencies
->getMock();
$twilioStub->expects($this->once())->method('retrieveLastTextFromBot')->willReturn($message);
$sut = new Sut($twilioStub);
$result = $sut->method();
}
}
Result
Execute the tests:
vendor/bin/phpunit tests/
Please verify if this code is working for you also. If not, please tell me what composer library versions you use. Consider adding this info to your question composer show -i.
The result should be:
PHPUnit 9.5.21 #StandWithUkraine
. 1 / 1 (100%)
Time: 00:00.006, Memory: 4.00 MB
OK (1 test, 1 assertion)
I expect you somehow call retrieveLastTextFromBot on something else than the mock object
If the code above is working, I suspect you somehow call the method retrieveLastTextFromBot on the production code, instead of the mock object or you might execute the method twice from a different location.
To find out what is really happening add the following code to retrieveLastTextFromBot (in your production code):
public function retrieveLastTextFromBot()
{
var_dump(debug_backtrace()[0]['file']); # This line
var_dump(debug_backtrace()[0]['line']); # This line
exit; # This line
$messages = $this->twilioClient->messages->read([
'to' => TwilioDefinitions::getToNumber(),
'from' => getenv('TWILIO_NUMBER'),
], 1);
if (count($messages) !== 1) {
throw new NoExtantTextException('No previous message from ' . getenv('TWILIO_NUMBER') . ' to ' . TwilioDefinitions::getToNumber());
}
return $messages[0];
}
If it returns the file where the SUT (System under test) is located, you now know for sure you don't have executed the method on the mock object. You might see a different file, in that case, check your code to see from where you execute it.
Check your tests to see what instance is passed in the constructor. If otherwise, let me know what the result of the debug_backtrace() is and consider updating your question with the results so we can continue to help, since not all code is provided it might be needed to help your further.
If non of the previous explained tests gave you enough information, please consider adding a temporarily github repository with your code and maybe if possible some test environment credentials.
I followed this guide: https://symfony.com/doc/current/console/lockable_trait.html and implemented the command lock feature for my one of my commands to see how it works. It worked as described and then I was going to implement it for all of my commands. But the issue is that I have about 50 commands and:
I do not want spent time adding the necessary code to each command
I want to have the centralized management of commands locking. I mean, adding extra option to regular commands so that they will be used by my future management center. For now I will need a pretty simple option protected function isLocked() for a regular command which will help me to manage if a command should have lockable feature.
So, I went to the source of \Symfony\Component\Console\Command\LockableTrait and after some time created the following listener to the event console.command:
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;
class LockCommandsListener
{
/**
* #var array<string, Lock>
*/
private $commandLocks = [];
private static function init()
{
if (!class_exists(SemaphoreStore::class)) {
throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
}
}
public function onConsoleCommand(ConsoleCommandEvent $event)
{
static::init();
$name = $event->getCommand()->getName();
$this->ensureLockNotPlaced($name);
$lock = $this->createLock($name);
$this->commandLocks[$name] = $lock;
if (!$lock->acquire()) {
$this->disableCommand($event, $name);
}
}
private function disableCommand(ConsoleCommandEvent $event, string $name)
{
unset($this->commandLocks[$name]);
$event->getOutput()->writeln('The command ' . $name . ' is already running');
$event->disableCommand();
$event->getCommand()->setCode()
}
private function createLock(string $name): LockInterface
{
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
}
return (new LockFactory($store))->createLock($name);
}
private function ensureLockNotPlaced(string $name)
{
if (isset($this->commandLocks[$name])) {
throw new LogicException('A lock is already in place.');
}
}
}
I made some tests and it kind of worked. But I am not sure this is the right way of doing things.
Another problem is that I can not find the proper exit code when I disabled a command. Should I just disable it? But it seems that exit code would be a great feature here. Specially when it comes to this listener testing (PHPUnit testing).
And I also have with testing itself. How can I run commands in parallel in my test class. For now I have this:
class LockCommandTest extends CommandTest
{
public function testOneCommandCanBeRun()
{
$commandTester = new ApplicationTester($this->application);
$commandTester->run([
'command' => 'app:dummy-command'
]);
$output = $commandTester->getDisplay();
dd($output);
}
}
It will allow only to run my commands one by one. But I would like to run them both so after running the first one, the second will fail (with some exit code).
As for me the best way to make background task is doing it via supervisor, create config file, like:
[program:your_service]
command=/usr/local/bin/php /srv/www/bin/console <your:app:command>
priority=1
numprocs=1
# Each 5 min.
startsecs=300
autostart=true
autorestart=true
process_name=%(program_name)s_%(process_num)02d
user=root
this is the best way to be sure that your command will be ran only in one process
I'm making a simple laravel package to wrap bash executions and make it be able to mock as laravel facade, but I have a problem.
Most of Php functions that runs bash commands uses pass-by-reference parameters, and the question is, how can I mock the return value of the pass-by-reference of the method?
MyFacade:
class MyFacade
{
public function run(string $command, &$return_val): string
{
return system($command, $return_val);
}
}
My test:
$passByReferenceValue = 1;
MyFacade::shouldReceive('run')->with('ping google.com', $passByReferenceValue)
->once()->andReturn("PING RESULT");
$return = MyFacade::run('ping google.com', $passByReferenceValue);
$this->assertEquals(1, $passByReferenceValue);
$this->assertEquals("PING RESULT", $return);
It actually works because I set the value of $passByReferenceValue and it's not changed. What I want is to pass a null pointer as second parameter of run method and make the mock to change it.
Example:
MyFacade::shouldReceive('run')->with('ping google.com', SOME_MAGIC_CODE_RETURNS_1)
->once()->andReturn("PING RESULT");
$resultCode = null;
$return = MyFacade::run('ping www.google.com', $resultCode)
$this->assertEquals(1, $resultCode);
$this->assertEquals("PING RESULT", $return);
Thanks
Is there any way I can pass parameters (like Query string in URL, or URL parameters) to a PHP file which is being run via CLI? I need this for some PHP cron jobs which need input from parameters. For example:
$ php /home/abc/www/myphp.php?param1=abc
There are two special variables in every command line interface argc and argv.
argv - array of arguments passed to the script.
argc - the number of command line parameters passed to the script (if run on the command line).
Make script cli.php
<?php
print_r($_SERVER['argv']);
and call it with argument:
$ php cli.php argument1=1
You should get the output looking like:
Array
(
[0] => cli.php
[1] => argument1=1
)
Source: http://www.php.net/manual/en/reserved.variables.server.php
If you are still so demanding to have a single point entry and to be able to process the url query as $_GET make your script act like a router by adding a switch:
if (PHP_SAPI === 'cli')
{
// ... BUILD $_GET array from argv[0]
}
But then - it violates SRP - Single Responsibility Principle! Having that in mind if you're still so demanding to make it work like you stated in the question you can do it like:
if (PHP_SAPI === 'cli')
{
/** ... BUILD from argv[0], by parsing the query string, a new array and
* name it $data or at will
*/
}
else
{
// ... BUILD new $data array from $_GET array
}
After that convert the code to use $data array instead of $_GET
...and have a nice day!
If the underlying code absolutely requires $_GET (and does not rely on other HTTP features) you can write a simple wrapper that abuses the fact that isn't a read-only superglobal:
<?php
// Untested (tweak to your own needs)
foreach($argv as $i){
list($k, $v) = explode('=', $i, 2);
$_GET[$k] = $v;
}
require_once('/home/abc/www/myphp.php');
... and then schedule your proxy instead:
php /path/to/wrapper.php param1=abc
Here's an extracted/rewritten class from my CLI Application library using $_SERVER['argv'] in the same form that linux runs commands (foo.sh --param "value" --marap):
<?php
class ArgumentScanner {
private static $instance;
private $arguments;
public static function get() {
if (empty(self::$instance)) {
self::$instance = new ArgumentScanner();
}
return self::$instance;
}
public function __construct() {
$this->arguments = $this->parseArguments($_SERVER['argv']);
}
public function __isset($argument) {
return isset($this->arguments[$argument]);
}
public function __get($argument) {
return (isset($this->arguments[$argument]) ? $this->arguments[$argument] : null);
}
/**
* Is used to parse the contents of $_SERVER['argv']
* #param array $argumentsRaw The arguments from $_SERVER['argv']
* #return stdClass An object of properties in key-value pairs
*/
private function parseArguments($argumentsRaw) {
$argumentBuffer = '';
foreach ($argumentsRaw as $argument) {
if ($argument[0] == '-') {
$argumentBuffer = substr($argument, ($argument[1] == '-' ? 2 : 1));
$equalSign = strpos($argumentBuffer, '=');
if ($equalSign !== false) {
$argumentKey = substr($argumentBuffer, 0, $equalSign);
$argumentsParsed[$argumentKey] = substr($argumentBuffer, $equalSign + 1);
$argumentBuffer = '';
} else {
$argumentKey = $argumentBuffer;
$argumentsParsed[$argumentKey] = '';
}
} else {
if ($argumentBuffer != '') {
$argumentKey = $argumentBuffer;
$argumentsParsed[$argumentKey] = $argument;
$argumentBuffer = '';
}
}
}
return (object)$argumentsParsed;
}
}
?>
Use:
<?php
$argumentScanner = ArgumentScanner::get();
if (isset($argumentScanner->reset)) {
echo '--reset was passed!';
}
if (isset($argumentScanner->test)) {
echo '--test was passed as ' . $argumentScanner->test . '!';
}
if (isset($argumentScanner->foo)) {
echo '--foo was passed as ' . $argumentScanner->foo . '!';
}
if (isset($argumentScanner->bar)) {
echo '--bar was passed as ' . $argumentScanner->bar . '!';
}
?>
php script.php --foo "bar" --reset -test="hey you!"
Output:
--reset was passed!
--test was passed as hey you!
--foo was passed as bar!
Best solution there would be to refactor and abstract away the business logic, retain the original script as a wrapper for it. Then add a CLI script to wrap the same business logic for the CLI.
That way, you can use the normal CLI parameter handling: http://www.php.net/manual/en/reserved.variables.argv.php
I needed to do the same thing, pass query parameters to a php script to run as a cron job. The script was from a third party and I didn't want to modify or write a new script. The solution was to place the query string parameters in single quotes and replace the question mark with an ampersand. In my case, I pass three parameters.
php /home/abc/www/myphp.php '¶m1=abc¶m2=def¶m3=123'
I found the solution in this post, tried it and it works fine for me.
if (PHP_SAPI === 'cli')
{
parse_str(implode('&', array_slice($argv, 1)), $_GET);
}
This is solution that I use, if running via cli, put vars and values in $_GET,
seems that is working for simple tasks , not sure if work with php getopt etc
try wget and output to null
wget http://localhost/myphp.php?param1=abc -o /dev/null
I'm using Artisan::call() in one of my routes and would like to save the command output to a variable.
Is there any way to capture the STDOUT and STDERR generated by the artisan command?
This is a way:
use Symfony\Component\Console\Output\BufferedOutput;
Route::get('/test', function()
{
$output = new BufferedOutput;
Artisan::call('list', array(), $output);
return $output->fetch();
});
When running a command from inside another command, here is how to get all the styling:
public function handle()
{
Artisan::call('other:command', [], $this->getOutput());
}
Seems the previous answers don't work in Laravel 5.2 any more (not sure about 5.1)
You can now use Artisan::output();
$output = '';
if (!Schema::hasTable('migrations')) {
Artisan::call('migrate:install', array());
$output .= Artisan::output();
}
// Updates the migration, then seed the database
Artisan::call('migrate:refresh', array('--force' => 1));
$output .= Artisan::output();
Artisan::call('db:seed', array('--force' => 1));
$output .= Artisan::output();
dd($output);
I had a unique solution that works well for me
In my base Command class which all my other commands extend I overloaded the output functions and then also If I need to I can overload them again in any extended commands... obviously writeLogFile is another custom function i made, however you can do whatever you want there
public function info($message,$verbosity = null)
{
$this->writeLogFile($message);
parent::info($message);
}
public function warn($message,$verbosity = null)
{
$this->writeLogFile($message);
parent::warn($message);
}
public function error($message,$verbosity = null)
{
$this->writeLogFile($message);
parent::error($message);
}