Introductie
Laravel console commands moeten lichtgewicht blijven en gericht zijn op het afhandelen van command-line input/output, terwijl complexe business logic naar queued jobs verplaatst moet worden. Dit architectuurpatroon scheidt de verantwoordelijkheden tussen de command-laag (verantwoordelijk voor CLI-interactie) en de business logic-laag (afgehandeld door jobs), wat resulteert in beter onderhoudbare en schaalbare applicaties.
Waarom
-
Betere Performance: Commands die zware business logic synchroon uitvoeren kunnen timeouts veroorzaken of andere processen blokkeren. Jobs maken asynchrone verwerking mogelijk, wat de uitvoering van commands drastisch versnelt.
-
Verbeterde Herbruikbaarheid: Wanneer business logic in jobs wordt ondergebracht in plaats van in commands, wordt het eenvoudig voor controllers, andere jobs en commands om dezelfde logic te gebruiken.
-
Schaalbaarheid: Jobs kunnen verdeeld worden over meerdere workers en queues, wat betere benutting van resources en de mogelijkheid om verwerking op basis van vraag te schalen mogelijk maakt.
-
Verbeterde Foutafhandeling: Jobs bieden robuuste mechanismen voor foutafhandeling, waaronder automatische retry-mogelijkheden met configureerbare backoff-strategieën, afhandeling van mislukte jobs en uitgebreide monitoring.
-
Efficiëntie van de Scheduler: Wanneer meerdere geplande commands tegelijkertijd draaien, voorkomen lichtgewicht commands die snel jobs dispatchen dat de scheduler geblokkeerd raakt, zodat alle geplande taken op tijd draaien.
-
Beter Testen: Business logic in jobs kan onafhankelijk van command-line-aspecten getest worden, wat leidt tot meer gerichte en onderhoudbare tests.
Geschikt voor
- Geplande Commands: Taken die op een schema draaien en dataverwerking, imports, exports of andere tijdrovende operaties uitvoeren
- Langlopende Operaties: Commands die grote datasets verwerken, meerdere API-calls maken of complexe berekeningen uitvoeren
- Operaties die Retry-Logic Vereisen: Taken die kunnen mislukken door externe afhankelijkheden (API's, third-party services)
- Batchverwerking: Commands die meerdere items moeten verwerken waarbij elk item onafhankelijk verwerkt kan worden
- Resource-Intensieve Taken: Operaties die significant geheugen of CPU-resources verbruiken
Minder geschikt voor
- Interactieve Commands: Commands die tijdens de uitvoering gebruikersinvoer of realtime feedback vereisen
- Eenvoudige Database Queries: Snelle operaties zoals het legen van de cache of eenvoudige database-opschoningen die binnen enkele seconden klaar zijn
- Development/Debugging Commands: Eenmalige commands die gebruikt worden voor debugging- of ontwikkeldoeleinden
- Commands die Directe Resultaten Vereisen: Operaties waarbij het command moet wachten op het resultaat en dit direct moet weergeven
Implementatie
Basispatroon
Command (Klein en Gericht):
namespace App\Console\Commands;
use App\Jobs\ProcessDataJob;
use Illuminate\Console\Command;
class ProcessDataCommand extends Command
{
protected $signature = 'data:process {type}';
protected $description = 'Queue data processing job';
public function handle()
{
$type = $this->argument('type');
// Minimal logic - just dispatch the job
ProcessDataJob::dispatch($type);
$this->info("Data processing job queued for type: {$type}");
return 0;
}
}
Job (Bevat Business Logic):
namespace App\Jobs;
use App\Services\DataProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessDataJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 120;
public function __construct(
private string $type
) {}
public function handle(DataProcessor $processor)
{
// All the heavy business logic goes here
$processor->processType($this->type);
}
public function failed(\Throwable $exception)
{
// Handle job failure
\Log::error('Data processing failed', [
'type' => $this->type,
'error' => $exception->getMessage()
]);
}
}
Geplande commands met jobs
// In app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// Good: Command quickly dispatches job
$schedule->command('data:process daily')
->daily()
->withoutOverlapping();
// Better: Direct job scheduling for simple cases
$schedule->job(new ProcessDataJob('hourly'))
->hourly();
}
Dubbele jobs voorkomen
Gebruik voor geplande commands unieke jobs om overlap te voorkomen:
use Illuminate\Contracts\Queue\ShouldBeUnique;
class ScheduledImportJob implements ShouldQueue, ShouldBeUnique
{
public function uniqueId(): string
{
return 'scheduled-import-' . $this->importType;
}
public function uniqueFor(): int
{
return 3600; // 1 hour
}
}
Veelvoorkomende valkuilen
De sync queue driver gebruiken
De belangrijkste valkuil is het gebruik van de standaard sync-driver, die jobs synchroon uitvoert en zo het hele doel van dit patroon tenietdoet:
// BAD: Job blocks the command with sync driver
QUEUE_CONNECTION=sync
// GOOD: Job runs asynchronously
QUEUE_CONNECTION=redis # or database, sqs, beanstalkd
Bij gebruik van de sync-driver:
- Jobs worden direct uitgevoerd in hetzelfde proces als het command
- Commands blokkeren totdat de job voltooid is
- Er zijn geen retry-mogelijkheden beschikbaar
- Mislukte jobs worden niet bijgehouden
- Je verliest alle voordelen van het verplaatsen naar jobs
Zorg er altijd voor dat je queue-connection geconfigureerd is voor asynchrone verwerking wanneer je dit patroon implementeert.