Usare i job invece dei comandi nello schedule di un'app Laravel
In una applicazione Laravel standard, puoi pianificare i comandi Artisan nel tuo console kernel. Anche se questo funziona per la maggior parte dei progetti, in Mailcoach abbiamo scelto una strada diversa. Invece di pianificare comandi, pianifichiamo dei job.
In questo post del blog vorrei spiegare perché e come lo facciamo.
Usare i comandi in uno schedule
Usando lo scheduler di Laravel, puoi eseguire i comandi a intervalli specifici. Il mio blog freek.dev è alimentato da una tipica applicazione Laravel. Diamo un'occhiata a una parte del suo schedule.
namespace App\Console;
// imports...
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
$schedule->command(RunHealthChecksCommand::class)->everyMinute();
$schedule->command('responsecache:clear')->daily();
$schedule->command('site-search:crawl')->daily();
$schedule->command('backup:run')->dailyAt('3:00');
// other commands
}
// ...
}
Probabilmente hai già molta familiarità con questo approccio. Puoi usare il metodo command su $schedule per pianificare qualsiasi comando desideri, e puoi concatenare un intervallo. L'unica cosa da notare qui è che, invece di passare una firma del comando, puoi anche passare il nome completo della classe di un Command.
Rendere i comandi tenant-aware
Nelle applicazioni tipiche, la maggior parte delle funzionalità è codificata nell'app stessa. La versione hosted di Mailcoach, chiamiamola Mailcoach Cloud, è un tipo speciale di applicazione Laravel. La maggior parte delle funzionalità è fornita dal package spatie/laravel-mailcoach, che puoi acquistare se vuoi self-hostare Mailcoach.
Il package spatie/laravel-mailcoach contiene un paio di comandi Artisan che dovrebbero essere pianificati come spiegato nella nostra documentazione di installazione.
/*
* in app/Console/Kernel.php of an app
* where you use spatie/laravel-mailcoach
*/
protected function schedule(Schedule $schedule)
{
// ...
$schedule->command('mailcoach:send-automation-mails')->everyMinute()->withoutOverlapping()->runInBackground();
$schedule->command('mailcoach:send-scheduled-campaigns')->everyMinute()->withoutOverlapping()->runInBackground();
$schedule->command('mailcoach:run-automation-triggers')->everyMinute()->runInBackground();
$schedule->command('mailcoach:run-automation-actions')->everyMinute()->runInBackground();
$schedule->command('mailcoach:calculate-statistics')->everyMinute();
$schedule->command('mailcoach:calculate-automation-mail-statistics')->everyMinute();
$schedule->command('mailcoach:rescue-sending-campaigns')->hourly();
$schedule->command('mailcoach:send-campaign-summary-mail')->hourly();
$schedule->command('mailcoach:cleanup-processed-feedback')->hourly();
$schedule->command('mailcoach:send-email-list-summary-mail')->mondays()->at('9:00');
$schedule->command('mailcoach:delete-old-unconfirmed-subscribers')->daily();
}
L'applicazione Mailcoach Cloud è multi-tenant e usa un unico DB. Ogni team che si registra su Mailcoach Cloud è considerato un tenant. Dietro le quinte, usiamo il nostro package spatie/laravel-multitenancy per rendere Mailcoach Cloud tenant-aware.
In Mailcoach Cloud, tutti i comandi Artisan elencati sopra devono essere eseguiti per ogni tenant. Avremmo potuto creare una versione speciale di ciascun comando che tiene conto del tenant. Ma preferiamo non farlo ed eseguire esattamente gli stessi comandi presenti in spatie/laravel-mailcoach.
La soluzione che abbiamo adottato qui è quella di iterare su ogni tenant e avvolgere ciascun comando in un job. Diamo un'occhiata al codice semplificato:
/*
* in app/Console/Kernel.php of mailcoach.app
*/
protected function schedule(Schedule $schedule)
{
Team::with(['subscriptions'])
->select(['id', 'name', 'trial_ends_at'])
->each(function (Team $team) use ($schedule): void {
$schedule
->job(new TeamArtisanJob($team, 'mailcoach:send-automation-mails'))
->name("{$team->name} - mailcoach:send-automation-mails")
->everyMinute();
$schedule
->job(new TeamArtisanJob($team, 'mailcoach:run-automation-triggers'))
->name("{$team->name} - mailcoach:run-automation-triggers")
->everyMinute();
$schedule
->job(new TeamArtisanJob($team, 'mailcoach:run-automation-actions'))
->name("{$team->name} - mailcoach:run-automation-actions")
->everyMinute();
// all other mailcoach commands.
});
}
Ogni comando è racchiuso in un TeamArtisanJob. Usiamo job in coda invece dei comandi perché, se ci sono molti tenant, eseguire tutti i comandi per tutti i tenant in sequenza potrebbe richiedere troppo tempo. Utilizzando job in coda, possiamo svolgere il lavoro necessario in modo concorrente grazie a più queue worker.
Diamo un'occhiata al codice semplificato di TeamArtisanJob e scopriamo cosa succede al suo interno. Ho omesso alcuni dettagli per rendere l'esempio più chiaro.
namespace App\Domain\Team\Jobs;
// imports left out for brevity...
class TeamArtisanJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public function __construct(
private Team $team,
private string $command,
)
{
}
public function handle()
{
$this->team->execute(fn () => Artisan::call($this->command));
Team::current()?->forget();
}
public function uniqueId()
{
return $this->team->id.$this->command;
}
public function uniqueFor()
{
return 60 * 15; // 15 minutes
}
}
Puoi vedere che il comando artisan stesso viene passato a una closure data al metodo execute su $this->team. Questo metodo è disponibile perché il modello Team estende la classe base Tenant fornita da spatie/laravel-multitenancy.
Il metodo execute eseguirà prima tutte le attività per rendere un tenant quello corrente. L'attività più importante per questo esempio è l'impostazione dei global scope. Dopo quell'attività, ogni query riceve una clausola where aggiuntiva che assomiglia a questa (pseudo codice) where team->id = <team-id-where-execute-is-being called-on>.
Dopo che il global scope è stato impostato, la closure passata a execute verrà eseguita. Nel nostro caso, verrà chiamato Artisan::call($this->command) (e ricorda, $this->command contiene una firma di comando del package spatie/laravel-mailcoach, ad esempio 'mailcoach:send-automation-mails'). Ogni query all'interno del comando sarà limitata al team corretto. Dopo l'esecuzione della closure, il global scope verrà rimosso automaticamente dal package spatie/laravel-multitenancy.
Quindi, con questa configurazione, abbiamo reso tenant-aware tutti i comandi del package spatie/laravel-mailcoach senza modificare il codice sorgente di quei comandi.
Nel codice di TeamArtisanJob vedi anche che stiamo usando il trait ShouldBeUnique. Questo garantisce che solo un job per questo comando e questo team sia presente in coda. Se la nostra coda dovesse essere così lenta che il TeamArtisanJob non viene eseguito in tempo, rieseguire lo scheduler non porterà a un job duplicato in coda.
In conclusione
Nella maggior parte delle applicazioni non avrai bisogno della strategia spiegata sopra: potresti creare comandi che svolgono il lavoro necessario per tutti i tenant in una sola volta (oppure lasciare che quei comandi facciano dispatch dei job che svolgono tutto il lavoro).
Per Mailcoach Cloud, che usa il package spatie/laravel-mailcoach self-hosted come suo cuore, questo è un modo eccellente per rendere multi-tenant aware quei comandi che non erano stati scritti pensando alla multi-tenancy.
Anche se non stai creando un'app multi-tenant aware, potrebbe essere utile eseguire i normali comandi pianificati come job, così che possano essere eseguiti in modo concorrente. Laravel rende facile pianificare job in coda.
Altri articoli recenti
Laravel AI 1.1 e 1.2: Agent Skills, Cohere e structured output più solidi
Le release 1.1.0 e 1.2.0 di laravel/ai portano gli Agent Skills, l'approvazione obbligatoria per i tool built-in e nuovi provider come Cohere e Laya self-hosted. Sul fronte qualità arrivano i fix allo structured output per Bedrock e il supporto alle provider options. Aggiornati anche i modelli di default, con Claude 4.7+ che ignora i sampling params.
Laravel 13.35.0: scheduler più controllabile, code più flessibili e response Markdown
Laravel 13.35.0 porta Schedule::alwaysOnOneServer(), il supporto a schedule:interrupt per i comandi di lunga durata, --memory in percentuale per queue:work e il nuovo metodo markdown() sulla response factory. Tante piccole migliorie anche a test, view compilate in parallelo e dischi local.
BashBox: un interprete Bash in sandbox per agenti AI in PHP
BashBox è un interprete bash scritto in puro PHP che permette di eseguire gli script generati dagli agenti AI senza mai ricorrere a proc_open, exec o shell_exec. Ogni comando è una classe PHP che lavora su un filesystem virtuale, con limiti di esecuzione, quote disco e accesso di rete configurabili. Ideale per chi sviluppa agenti con il Laravel AI SDK o altre librerie simili.