How to schedule Artisan commands in a package? - laravel

I have a package that contains Artisan commands. I’ve registered these commands with Artisan via my service provider like so:
/**
* Register the application services.
*
* #return void
*/
public function register()
{
// Register Amazon Artisan commands
$this->commands([
'App\Marketplace\Amazon\Console\PostProductData',
'App\Marketplace\Amazon\Console\PostProductImages',
'App\Marketplace\Amazon\Console\PostProductInventory',
'App\Marketplace\Amazon\Console\PostProductPricing',
]);
}
However, these commands need to be scheduled to run daily.
I know in app/Console/Kernel.php there is the schedule() method where you can register commands and their frequency, but how can I schedule commands in my package’s service provider instead?

(see Dave's answer below for Laravel 6.10+)
The trick is to wait until after the Application has booted to schedule the commands, since that is when Laravel defines the Schedule instance and then schedules commands internally. Hope this saves someone a few hours of painful debugging!
use Illuminate\Support\ServiceProvider;
use Illuminate\Console\Scheduling\Schedule;
class ScheduleServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->booted(function () {
$schedule = $this->app->make(Schedule::class);
$schedule->command('some:command')->everyMinute();
});
}
public function register()
{
}
}

In Laravel 6.10 and above:
use Illuminate\Support\ServiceProvider;
use Illuminate\Console\Scheduling\Schedule;
class ScheduleServiceProvider extends ServiceProvider
{
public function boot()
{
$this->callAfterResolving(Schedule::class, function (Schedule $schedule) {
$schedule->command('some:command')->everyMinute();
});
}
public function register()
{
}
}

Related

Is there a way to disable artisan commands?

Is there a way to disable artisan commands from running at all?
For example, if I wanted to disable php artisan migrate:fresh from running, where would I go to remove/disable the command?
As far as I know, laravel does not have this feature by default. And this is still under laravel ideas.
I also had this problem before and could not find a solution, I am not sure why you want to disable a command. But my case was that in the production environment I never want to run php artisan migrate:fresh. So what I end up with is to override the default command.
For example, in the routes/console.php file:
if ('production' === App::environment()) {
Artisan::command('migrate:fresh', function () {
$this->comment('You are not allowed to do this in production!');
})->describe('Override default command in production.');
}
So, when you are in production, php artisan migrate:fresh will do nothing. You can change the condition based on your requirement, my example is just an idea of how you can override a laravel default command based on some variables in the .env file.
You can do a lot of things here as well, I am not sure why you want to disable the command, so this is the best I can help.
Create a command like the following
<?php
namespace App\Console\Commands\Utils;
use Illuminate\Console\Command;
use Illuminate\Console\Events\CommandStarting;
class PreCommand extends Command
{
protected $signature = 'precommand';
public function handle(CommandStarting $event) {
if (app()->environment(['production'])) {
logger('Trying to fresh database in production');
if ($event->command == 'migrate:fresh') {
$this->output = $event->output;
$this->info('You can not fresh database in the production');
die();
}
}
}
}
And register it in your EventServiceProvider's boot method
<?php
namespace App\Providers;
use App\Console\Commands\Utils\PreCommand;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* Register any events for your application.
*
* #return void
*/
public function boot() {
Event::listen(CommandStarting::class, PreCommand::class);
}
}

Laravel actingAs guest

Laravel provides a way to authenticate a given user during HTTP testing with
$this->actingAs($user);
Is there a way to unauthenticate that $user within the same test?
Yes, you can unauthenticate using this:
Auth::logout();
https://laravel.com/docs/7.x/authentication#logging-out
Warning: above does far more than just forgetting (acting as if the login did not happen), for example, when using JWT above should invalidate token.
Yes, define new actingAsGuest method in base TestCase class
in file tests/TestCase.php
<?php
namespace Tests;
use Illuminate\Auth\RequestGuard;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function setUp(): void
{
parent::setUp();
// add logout method to RequestGuard
RequestGuard::macro('logout', function() {
$this->user = null;
});
}
// add method to base TestCase class
public function actingAsGuest(): void
{
$this->app['auth']->logout();
}
}
And then in your test class you can use it:
<?php
namespace Tests\Feature;
use App\Models\User;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* #return void
*/
public function test_example()
{
// acting as authenticated user
$this->actingAs(User::factory()->create());
$this->assertAuthenticated();
// acting as unauthenticated user
$this->actingAsGuest();
$this->assertGuest();
}
}
I had same requirements as OP did, but wanted actingAsGuest() to completely reset everything, except Database state.
Full App reset (except DB)
For Laravel 7 (and maybe newer or older)
I toke a look at Laravel's tearDown() and setUp() methods.
And came up with helper method like:
use Illuminate\Support\Facades\Facade;
// ...
function actingAsGuest()
{
// Backup database state.
/** #var \Illuminate\Database\MySqlConnection $connection */
$connection = app('db.connection');
// Reset everything else.
/** #var \Illuminate\Foundation\Application $app */
$app = $this->app;
$app->flush();
$this->app = null;
Facade::clearResolvedInstances();
$this->refreshApplication();
// Restore database state.
app('db')->extend($connection->getName(), function () use ($connection) {
return $connection;
});
}
WARNING !!
Above works fine unless your test's logic caches any of above discarded objects somewhere.
For example, Laravel's DatabaseTransactions trait did cache db facade (in their App-Destroyed-listener).
Which we fixed by overriding said trait's logic.
Like we changed:
// ...
$this->beforeApplicationDestroyed(function () use ($database) {
// ...
Into:
// ...
$this->beforeApplicationDestroyed(function () {
$database = app('db');
// ...

execute Queue job without paasing by my ViewComposer i share with (*) using pm2

trying to prevent queued job from passing by ViewComposer in laravel using pm2.
Problem : setCookie has not been applied because (i think ) there is no session from extern calls access (pm2).
let me explain:
also sharing with you this part of my app.
I have a queued job that sends dynamic emails (templates and mail params)
in my ServiceController,
also, am sharing needed data with all view when returning to any of them,
so I created a ViewComposer called by any view(*) in AppServiceProvider, inside this ViewComposer, am using a trait that uses many traits too, to collect data I need for views.
when I navigate in my application everything looks fine.
when implementing any task that uses queued jobs, in my case sending an email:
when I run php artisan queue:work everything works fine too and I receive emails.
BUT
when I use pm2 deamon which calls that queue it fires an exception :
(setCookie has not been applied) related to that trait I use in view composer.
I know it's hard for me to explain it, but I really need some help here.
here is my code :
pm2 path : / (server root)
app path :/prod
shared hosting with ssh.
1. AppServiceProvider
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
Schema::defaultStringLength(191);
}
public function boot()
{
view()->composer('*',
'App\Http\ViewComposers\MasterComposer');
}
}
2. WebsiteComposer:
namespace App\Http\ViewComposers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Website;
use other_traits;
trait WebsiteComposer
{
use other_traits;
protected function Get_Website(Request $request)
{
//some code by calling other traits;
// $results= get client websitestuff;
$this->website= $results;
}
}
3. MasterComposer:
namespace App\Http\ViewComposers;
use Illuminate\View\View;
use Illuminate\Http\Request;
class MasterComposer
{
use WebsiteComposer;
public $web=[];
public function __construct()
{
}
public function compose(View $view)
{
//to prevent external views( email templates located in
//views/emails from passing by getting website process...
if(!starts_with($view->getName(), 'emails'))
{
//here where exception fired using pm2
$this->Get_Website(request());
$this->web=$this->website;
}
View::share('website', $this->web);
}
}
4. ServiceContorller:
(the Start of the process)
// POST PROCESS then Calling this function :
public function EmailDispatcher($data,Request $request)
{
//some code...
dispatch(function () use ($data) {
dispatch(new GuestMailerJob($data));
});
return redirect('somview');
}
5. GuestMailerJob:
namespace App\Jobs;
use App\Mail\GuestMailer;
// needed uses...
class GuestMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable,
SerializesModels,GuestMailer;
protected $data;
public $tries = 3;
public $timeout = 10;
public function retryUntil()
{
return now()->addSeconds(12);
}
public function __construct(Array $data)
{
$this->data=$data;
}
public function handle()
{
$this->sendEmail($this->data);
}
}
6. GuestMailer:
namespace App\Mail;
use Illuminate\Http\Request;
use Mail;
trait GuestMailer
{
protected function sendEmail(array $all)
{
$toview = array(
'title' => $all["title"],
'other_attributes' => $all["other_attributes"]
);
Mail::send( [$all["template"]=> $all['view']],$toview,
function ($message) use($all)
{
$message->from( $all['from'],$all['nameFrom'] );
$message->subject( $all['subject'] );
//other params...
}
});
}
}
7. pm2 executed file : mail-worker.yml
//located in: (/prod)
//mail-worker.yml content:
apps:
- name: mail-worker
script: artisan
exec_mode: fork
interpreter: php
instances: 1
args:
- queue:work
- --tries=1
- --sleep=1
8 : artisan Cleaning
all php artisan cleaners are implemented
ex : php artisan clear-compiled
php artisan queue:restart
9 : pm2 Configuration :
pm2 start mail-worker.yml
pm2 monit
10 - pm2 state :
id name mode reload status cpu memory
0 mail-worker fork 1 online 0% 6.5mp
it's working with php artisan queue:work / listen
but with pm2 here is the exception.
pm2 Result :
Whoops\Run::handleError("Trait method setCookie has not been applied,
/app_base_path/PRODMAAN/vendor/filp/whoops/src/Whoops/Run.ph x
Whoops\Run::handleShutdown() [internal]:0
failed_Jobs result :
Illuminate\Queue\MaxAttemptsExceededException: App\Jobs\GuestMailerJob has been attempted too many times or run too long. The job may have previously timed out.
what is the problem ?
thank you for any advice about the code structure too.

Laravel and Local Tunnel integration

My idea was to use Local Tunnel's subdomain feature to expose callback URI in a more convenient way. However, I believe that I could've achieved the same results in a simper way.
This is the solution with Laravel Valet:
In package.json I've added a script called shared
"scripts": {
...
"share": "lt --port 80 --subdomain blog --local-host blog.test"
}
In AppServiceProvider.php I've extended the UrlGenerator to avoid redirecting back to http://blog.test
<?php
namespace App\Providers;
use App\Services\LocalTunnelUrlGenerator;
use Blade;
use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
(...)
}
public function register()
{
$this->enableLocalTunnel();
}
private function enableLocalTunnel()
{
if (!app()->environment('local') || !config('app.use_local_tunnel')) {
return;
}
$this->app->extend('url', function (UrlGenerator $defaultGenerator) {
/** #var Router $router */
$router = $this->app['router'];
$routes = $router->getRoutes();
return new LocalTunnelUrlGenerator($routes, $defaultGenerator->getRequest());
});
}
}
This is the the LocalTunnelUrlGenerator.php:
<?php
namespace App\Services;
use Illuminate\Http\Request;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\UrlGenerator;
class LocalTunnelUrlGenerator extends UrlGenerator
{
public function __construct(RouteCollection $routes, Request $request)
{
parent::__construct($routes, $request);
}
public function formatRoot($scheme, $root = null)
{
return "https://blog.localtunnel.me";
}
}
Why all that? Because whenever the application call the redirect() method, we are sent back to http://blog.test.
Do I really need to extend the UrlGenerator to make it work?

Laravel 5.2 - ServiceProvider boot not working

Trying to use service provider to set a variables throughout all views pages that #extends('layouts.app') but not working, first I show the codes below.
AppServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Auth;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
// Using class based composers...
view()->composer('layouts.app', function($view){
$view->with('current_user', Auth::user());
});
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
}
When I visit the page that calls {{$current_user}}, it shows the following error
ErrorException in 5a5612347179ad88a6d4ebacc5d911a184c1b4ed.php line 14:
Undefined variable: current_user (View: C:\xampp\htdocs\soyegg\resources\views\shops\edit\showroom.blade.php)
Went through a several questions solved in the website and checked the followings but still failed:
1. AppServiceProvider is in config.app (default by Laravel 5.2)
2. php artisan clear-compiled
3. php artisan optimize
4. check whether there is compiled.php and try to clear it mannually but there is in neither storage/framework nor vendor.
Please help!
I think your ServiceProvider is working fine, you only need to change the way you passing data to the view. I've tested it myself and I can confirm that passing data to a view which later will be included with #extends() won't work. Instead you can use another methods:
view()->share('current_user', Auth::user());
Or using wildcard:
view()->composer('*', function ($view) {
$view->with('current_user', Auth::user());
});
// wildcard with prefix
view()->composer('prefix.*', function ($view) {
$view->with('current_user', Auth::user());
});

Resources