Laravel avanzato: l’utilizzo di cron

Spread the love

Ho bisogno di eseguire un determinato metodo di un controller 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.

Estendere 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 metodo schedule() 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 directory app/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 metodo handle()

È tutto.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.