Can't add argument to custom command - laravel-5

I'm using laravel 5.1. in a debian bash shell. I created a custom console command called survey:complete. I've been using it for a while, and now I want to add an optional argument for the number of surveys to generate.
However, I've followed the documentation, but I haven't been able to successfully add my argument. I changed the signature likewise:
protected $signature = 'survey:complete {--number=}';
And tried to reference the argument
public function handle() {
for( $i = 0; $i < $this->argument('number'); $i++ ) {
But I get this error:
$> php artisan survey:complete --number=1
[InvalidArgumentException]
The "number" argument does not exist.
I print_r()'d the arguments array, and I get this:
$ php artisan survey:complete --number=1
Array(
[command] => survey:complete
)
[InvalidArgumentException]
The "number" argument does not exist.
How do I add my argument to my command?

I needed to use option(), not argument().
$number = $this->option('number');

Worked for me instead:
protected $signature = 'mycommand {limit=1000}'
// call: php artisan mycommand 100
I retrieve the value with:
public function handle() {
// ...
$limit = $this->arguments('limit')['limit']
// ...
}

Related

Interact with Artisan command programmatically

First of all, the question is kind of similar to this question, but unfortunately, it does not have an answer.
Consider having an interactive command like the following:
class SayHello extends Command
{
protected $signature = 'say:hello';
public handle()
{
$name = $this->ask('Your name');
$this->info('Hello, ' . $name . '!');
}
}
The question is: How to call this command programmatically (in controller, job, tinker, etc.) and answer the question using code (without real-time interaction)?
PS: I already know I can call an artisan command using the Artisan::call() method. I want to know how to handle interaction (questions, choices, etc.)
Artisan::call can be used to execute commands programmatically. Eg:
//use Illuminate\Support\Facades\Artisan;
$name = "John Doe";
Artisan::call('say:hello', [
'name' => $name,
]);
//OR
Artisan::call('say:hello John');
//AND
Artisan::output(); //gives you the output
Call method's first argument is command signature and the second is an array of parameters.
Reference: https://laravel.com/docs/9.x/artisan#programmatically-executing-commands

run bash scripts using laravel command class gives not found error

I want to run simple bash scripts using laravel command class, but I have not found error.
If I go to /var/www/mysite/storage/app/scripts and run script from there in command line, then everything is OK.
$ sh remove-spaces.sh
What is wrong in my Laravel code?
lowercase.sh
for file in /var/www/mysite/storage/app/img/*;
do mv "$file" "`echo $file | tr '[A-Z]' '[a-z]'`";
done
remove-spaces.sh
for file in /var/www/mysite/storage/app/img/*;
do mv "$file" `echo $file | tr ' ' '-'`;
done
RenamePicturesCommand
namespace App\Console\Commands\Pictures;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
class RenamePicturesCommand extends Command
{
protected $removeSpaces;
protected $lowercase;
protected $signature = 'pictures:rename';
public function __construct()
{
parent::__construct();
$this->removeSpaces = new Process(['sh /var/www/mysite/storage/app/scripts/remove-spaces.sh']);
$this->lowercase = new Process(['sh /var/www/mysite/storage/app/scripts/lowercase.sh']);
}
public function handle()
{
$this->removeSpaces->run();
if (!$this->removeSpaces->isSuccessful()) {
throw new ProcessFailedException($this->removeSpaces);
}
echo $this->removeSpaces->getOutput();
$this->lowercase->run();
if (!$this->lowercase->isSuccessful()) {
throw new ProcessFailedException($this->lowercase);
}
echo $this->lowercase->getOutput();
}
}
error output
http#0bb690b74597:/var/www/mysite$ php artisan pictures:rename
Symfony\Component\Process\Exception\ProcessFailedException
The command "'sh /var/www/mysite/storage/app/scripts/remove-spaces.sh'" failed.
Exit Code: 127(Command not found)
Working directory: /var/www/mysite
Output:
================
Error Output:
================
sh: 1: exec: sh /var/www/mysite/storage/app/scripts/remove-spaces.sh: not found
at app/Console/Commands/Pictures/RenamePicturesCommand.php:59
55▕ // execute command
56▕ $this->removeSpaces->run();
57▕ // executes after the command finishes
58▕ if (!$this->removeSpaces->isSuccessful()) {
➜ 59▕ throw new ProcessFailedException($this->removeSpaces);
60▕ }
61▕ echo $this->removeSpaces->getOutput();
62▕
63▕
+13 vendor frames
14 artisan:37
Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
You may try to provide the command and argument as two separate items in the commands array
namespace App\Console\Commands\Pictures;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
class RenamePicturesCommand extends Command
{
protected $removeSpaces;
protected $lowercase;
protected $signature = 'pictures:rename';
public function __construct()
{
parent::__construct();
$this->removeSpaces = new Process(['sh', '/var/www/mysite/storage/app/scripts/remove-spaces.sh']);
$this->lowercase = new Process(['sh', '/var/www/mysite/storage/app/scripts/lowercase.sh']);
//Or you can use Process::fromShellCommandline
//$this->removeSpaces =Process::fromShellCommandline('sh /Volumes/samsung-250/Sites/stackoverflow/storage/app/scripts/lowercase.sh');
//$this->lowercase =Process::fromShellCommandline('sh /Volumes/samsung-250/Sites/stackoverflow/storage/app/scripts/remove-spaces.sh');
}
public function handle()
{
$this->removeSpaces->run();
if (!$this->removeSpaces->isSuccessful()) {
throw new ProcessFailedException($this->removeSpaces);
}
echo $this->removeSpaces->getOutput();
$this->lowercase->run();
if (!$this->lowercase->isSuccessful()) {
throw new ProcessFailedException($this->lowercase);
}
echo $this->lowercase->getOutput();
}
}
You can also achieve the same results with Storage facade without any shell script
namespace App\Console\Commands\Pictures;
use Illuminate\Console\Command;
use Illuminate\Support\LazyCollection;
class RenamePicturesCommand extends Command
{
protected $signature = 'pictures:rename';
public function handle()
{
$this->info('Renaming pictures...');
$this->newLine(2);
$files = LazyCollection::make(Storage::disk('local')->files('img'))
//Remove dotfiles
->reject(fn($filePath) => Str::startsWith($filePath, '.'));
$progress = $this->output->createProgressBar($files->count());
$progress->start();
$modified = [];
$files->each(function ($file) use ($progress, &$modified) {
$new = strtolower(preg_replace('/\s+/', '-', $file));
$temp = ['old' => $file, 'new' => $new, 'status' => 'success'];
if(! Storage::disk('local')->move($file, $new)) {
$temp['status'] = 'failed';
$temp['new'] = $file;
}
$modified[] = $temp;
$progress->advance();
});
$this->newLine(2);
$this->info('Finished Renaming pictures');
$this->newLine(2);
$this->table(['Old', 'New', 'Status'], $modified);
}
}

Testing interactive artisan commands using Mockey

I am writing unit tests for interactive commands in Laravel 5.3 following this guide but I can't seem to get Mockery to work.
I've set up a simple command, greet:user.
public function handle()
{
if(!$name = $this->argument('name')) {
$name = $this->ask('Name of user to greet');
}
$this->info("Hello {$name}.");
}
And am writing unit-tests like;
public function testCanGreetGivenUser()
{
$command = Mockery::mock('App\Console\Commands\GreetUser[info]');
$command->shouldReceive('info')->once()->with('Hello Brian.');
$exit_code = Artisan::call('greet:user', ['name' => 'Brian', '--no-interaction' => true]);
$this->assertEquals(trim(Artisan::output()), 'Hello Brian.');
$this->assertEquals($exit_code, 0);
}
Issue:
Mockery\Exception\InvalidCountException: Method info("Hello Brian.") from Mockery_0_App_Console_Commands_GreetUser should be called
exactly 1 times but called 0 times.
My goal is to test;
If no input expected
$this->artisan('greet:user', ['name' => 'Brian'])
->expectsOutput('Hello Brian.')
->assertExitCode(0);
If input is required.
$this->artisan('greet:user')
->expectsQuestion('Name of user to greet', 'James')
->expectsOutput('Hello James.')
->assertExitCode(0);
That's how I could do it in Laravel 5.7, but how can I achieve the same for Laravel 5.3
To solve the problem you'll have to call the test as follows:
public function testCanGreetGivenUser()
{
$command = Mockery::mock('\App\Console\Commands\GreetUser[info]');
$command->shouldReceive('info')->once()->with('Hello Brian.');
$this->app[\Illuminate\Contracts\Console\Kernel::class]->registerCommand($command);
$exit_code = $this->artisan('greet:user', ['name' => 'Brian', '--no-interaction' => true]);
$this->assertEquals($exit_code, 0);
}
The deciding factor is the command registration. You must do it to actually replace the existing instance of the command with your mock.

laravel controller cannot use currentRouteName() inside construce()

I have named routes:
admin.post.category
admin.post.tag
admin.post.theme
routes\admin.php
Route::group(['prefix' => 'post', 'as' => 'post.'], function () {
Route::resource('category', 'Admin\Post\TermsController');
Route::resource('theme', 'Admin\Post\TermsController');
Route::resource('tag', 'Admin\Post\TermsController');
});
Http\Controller\Admin\Post\TermsController
public function __construct(Request $request)
{
$this->request = $request;
$route_name = Route::currentRouteName();
$arr = explode('.', $route_name);
echo "<pre>".print_r($arr[2], 1)."</pre>"; exit;
}
When I visit the page http://localhost/admin/post/category, it can show category. But when I run command
php artisan route:list
It shows
[ErrorException]
Undefined offset: 2
If I move the three lines from __construct() to index()
$route_name = Route::currentRouteName();
$arr = explode('.', $route_name);
echo "<pre>".print_r($arr[2], 1)."</pre>"; exit;
Then "php artisan route:list" runs well.
How to fix this?
It's throwing the error because you have some routes with names that contain only 1 dot. So when you access $arr[2] it's undefined and thus the error Undefined offset: 2. Looks like your shoving every route method into a single controller. When you try to run php artisan route:list it checks every route, thereby invoking the constructor, so it fails for routes with names with just 1 dot. Change your code to this and it should work.
$route_name = \Route::currentRouteName();
$arr = explode('.', $route_name);
if (isset($arr[2])) {
echo "<pre>".print_r($arr[2], 1)."</pre>";
}
exit;

Testing Laravel (5.1) console commands with phpunit

What is the best way to test Laravel console commands?
Here is an example of a command I'm running. It takes in a value in the constructor and in the handle method.
class DoSomething extends Command
{
protected $signature = 'app:do-something';
protected $description = 'Does something';
public function __construct(A $a)
{
...
}
public function handle(B $b)
{
...
}
}
In my test class, I can mock both A and B, but I can't figure out how to pass $a in.
$this->artisan('app:do-something', [$b]);
Is it possible? Or am I going about this all wrong? Should I pass everything in thought the handle() method?
Thanks.
You will have to change around how you call the command in testing, but it is possible to mock an object passed through.
If the class used by Artisan is dependency-injected like this:
public function __construct(ActualObject $mocked_A)
{
//
}
Then write up the test case like this:
$mocked_A = Mockery::mock('ActualObject');
$this->app->instance('ActualObject', $mocked_A);
$kernel = $this->app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArrayInput([
'command' => 'app:do-something',
]),
$output = new Symfony\Component\Console\Output\BufferedOutput
);
$console_output = $output->fetch();
The $this->app->instance('ActualObject', $mocked_A); line is where you are able to call upon and use the mocked version of your class, or object, instead of the actual.
This will work in Laravel or Lumen.

Resources