Ho bisogno di eseguire un determinato metodo di un controller di un’applicazione sviluppata in Laravel regolarmente una volta al giorno.
Avevo fatto un programma bash e l’avevo inserito nella crontab, ci dovevo fare un aggiornamento quotidiano del mio DB da una sorgente dati.
Questo database mi serve per una applicazione live, per cui devo rendermi autonomo da operazioni manuali.
Avrei potuto replicare la modalità anche in remoto, ma ho preferito farlo attraverso Laravel, tirando dentro all’applicazione tutta la logica eseguita nello script di shell.
Un primo livello è stato quello di scrivere dei programmi PHP che leggevano la sorgente e aggiornavano il db attraverso un nuovo metodo dei miei controller dedicati a quelle entità relazionali.
Quindi potevo lanciare manualmente invocando un URL la funzionalità e tutto filava meravigliosamente.
L’ultimo passo è rendere autonoma l’applicazione Laravel perché ad una certa ora del giorno esegua il task. Qui arriva l’utilissimo Scheduler di Laravel.
L’idea di fondo è quella di invocare in modalità cron la rotta URI che scatena il metodo del controller. Questa funzionalità di Laravel si basa comunque sul clock del sistema operativo: un cron batch che viene lanciato ogni minuto e che viene preso come riferimento da Laravel per temporizzazioni diverse. Quindi è comunque obbligatorio agire su due fronti:
- sullo scheduler di Laravel e
- sul crontab di sistema.
Vediamo come si fa.
Sommario
Estendere Laravel/Artisan creando un comando che chiama una rotta
Premetto che sto utilizzando questa versione di Laravel:
$ php artisan --version Laravel Framework 6.18.8
Possiamo estendere le funzionalità di artisan
che usiamo correntemente come ad esempio:
$ php artisan make:controller InvoiceController
per fare in modo che si possa invocare un nostro comando artisan
; quindi la prima cosa da fare è creare un nuovo comando artisan
che chiamo route:call. Ho bisogno inoltre che a questo comando venga affiancata una serie di parametri opzionali.
Esistono già dei comandi artisan
per gestire le rotte; se provo a lanciare il mio comando ancora da definire ho questa risposta:
$ php artisan route:call Command "route:call" is not defined. Did you mean one of these? route:cache route:clear route:list
Per creare un nuovo comando artisan
si fa così:
$ php artisan make:command RouteCall
Laravel creerà un nuovo comando che consiste in una classe che si chiamerà come il primo parametro (RouteCall
) e la sintassi (signature) sarà definita all’interno del file contenente la nuova classe, che viene creata dentro la cartella app/Console/Commands
ed estende la classe Command:
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Request; class RouteCall extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'route:call {--uri=}'; /** * The console command description. * * @var string */ protected $description = 'Call route from CLI'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { $request = Request::create($this->option('uri'), 'GET'); $this->info(app()['Illuminate\Contracts\Http\Kernel']->handle($request)); } protected function getOptions() { return [ ['uri', null, InputOption::VALUE_REQUIRED, 'The path of the route to be called', null], ]; } }
In particolare osserviamo la sintassi del comando contenuta nella proprietà $signature
che è quella che abbiamo definito nel creare la class nel comando sopra a cui però ho aggiunto il parametro {--uri}
che è l’URI del metodo da chiamare. All’interno della classe interessata aggiungerò anche una gestione del parametro in modo tale da renderlo opzionale: in pratica il parametro è una data in formato YYYY-MM-DD
: se non la passo nell’URI, sottointendo data odierna.
Il metodo chiave è handle()
che trasforma un comando Linux in una request HTTP. Come si vedrà adesso il controllo passa al modulo Kernel.php.
Schedulazione del comando
Il secondo passaggio è quello di definire la temporizzazione del comando:
<?php // file app/Console/Kernel.php namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { /** * The Artisan commands provided by your application. * * @var array */ protected $commands = [ 'App\Console\Commands\CallRoute', ]; /** * Define the application's command schedule. * * @param \Illuminate\Console\Scheduling\Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->command('call:route')->dailyAt('19:00'); } /** * Register the commands for the application. * * @return void */ protected function commands() { $this->load(__DIR__.'/Commands'); require base_path('routes/console.php'); } }
Il nodo centrale è il metodo schedule()
che contiene uno o più comandi, ognuno con la temporizzazione desiderata, in questo caso voglio che il Kernel dello scheduler avvii il comando route:call
ogni giorno alle 19:00.
Avvio dello scheduler
La terza operazione è quella di creare una nuova voce nella crontab
che avvia lo scheduler:
$ crontab -e ... # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command * * * * * cd path/to/project/ && php artisan schedule:run >> /dev/null 2>&1
questo job è chiamato con la risoluzione massimo di 1 volta al minuto, poi la logica di Laravel farà in modo che i singolo job vengano eseguiti su temporizzazioni diverse – come sono state definite nel metodo Kernel->schedule()
– ma sempre con il clock di riferimento dettato dalla crontab.
Quindi riassumendo, a partire dalla causa primaria, fino all’effetto ultimo:
- crontab invoca lo scheduler di Laravel ogni minuto;
- lo scheduler controlla se c’è quacosa da eseguire in base alle diretive
everyMinute(), dailyAt(), daily()
eccetera definite per i comandi invocati nel metodoschedule()
del Kernel; - il Kernel avvia tutti i comandi registrati nel metodo
schedule()
; - ognuno di questi metodi è invocato con un comando
artisan
(es.route:call
) ma in generale potrebbe anche essere una chamata a Eloquent per scrivere qualcosa nel database; - il singolo comando
artisan
è definito in una classe specifica (es.RouteCall
) contenuta nella directoryapp/Console/Commands
/ - il metodo che esegue quanto si desidera (nel mio caso la trasformazione di un comando shell come
php artisan route:call
in una request HTTP) è il metodohandle()
È tutto.
Commenti recenti