Een gebruiker plaatst een bestelling en ziet direct een bevestiging in zijn browser. Ondertussen wordt er een e-mail verstuurd, de voorraad bijgewerkt, een PDF-factuur gegenereerd en een Slack-bericht naar het magazijn gestuurd. De gebruiker hoeft op niets te wachten. Dat is de kracht van event-driven PHP met queues.
In deze gids leer je hoe je PHP-applicaties ontwerpt waarbij events centraal staan en queues het zware werk op de achtergrond afhandelen. Je bouwt voort op eerdere kennis over asynchrone taken en queues en ontdekt hoe je een losgekoppelde, schaalbare architectuur neerzet.
Wat is event-driven architectuur?
Event-driven architectuur is een ontwerpstijl waarbij onderdelen van je applicatie communiceren door events te publiceren en af te luisteren, in plaats van elkaar rechtstreeks aan te roepen.
Een event is simpelweg een bericht dat iets beschrijft wat gebeurd is. Bijvoorbeeld: OrderPlaced, UserRegistered of PaymentFailed. Het event zegt niets over wat er vervolgens moet gebeuren, dat bepalen de listeners (of subscribers) die op dat event reageren.
Dit staat in contrast met de traditionele, imperatieve aanpak:
// Traditioneel: direct gekoppeld
$order = $orderService->place($data);
$mailer->sendConfirmation($order);
$inventory->decrease($order);
$invoiceService->generate($order);
$slackNotifier->notifyWarehouse($order);
In een event-driven opzet ziet dat er heel anders uit:
// Event-driven: losgekoppeld
$order = $orderService->place($data);
$eventDispatcher->dispatch(new OrderPlaced($order));
De OrderPlaced event triggert achter de schermen meerdere listeners. De controller weet niets van e-mails, voorraad of Slack. Dat maakt je code eenvoudiger te onderhouden en uit te breiden.
Waarom queues erbij halen?
Events alleen zijn niet genoeg. Als je listeners synchroon draaien, wacht de gebruiker alsnog op alle acties. En als één listener faalt (denk aan een externe API die traag reageert), crasht het hele proces.
Daarom combineer je events met een message queue. Het event wordt op een wachtrij geplaatst en een of meerdere workers pakken het later op. De HTTP-request van de gebruiker is klaar binnen milliseconden, terwijl de daadwerkelijke verwerking parallel doorloopt.
Deze aanpak sluit naadloos aan bij wat je geleerd hebt over PHP in high-performance systemen: PHP zelf is van oudsher synchroon, maar met queues omzeil je die beperking elegant.
Events versus jobs: het verschil
Een belangrijk onderscheid in event-driven PHP:
- Event: beschrijft iets dat gebeurd is. Verleden tijd. Bijvoorbeeld
OrderPlaced,InvoiceGenerated,UserRegistered. - Job: een concrete taak die uitgevoerd moet worden. Opdracht. Bijvoorbeeld
SendConfirmationEmail,GenerateInvoicePdf,UpdateInventory.
In de praktijk triggert één event meerdere jobs. De event-listener doet zelf geen zwaar werk, hij zet alleen jobs op de queue.
class OrderPlacedListener
{
public function handle(OrderPlaced $event): void
{
Queue::push(new SendConfirmationEmail($event->orderId));
Queue::push(new UpdateInventory($event->orderId));
Queue::push(new GenerateInvoicePdf($event->orderId));
Queue::push(new NotifyWarehouse($event->orderId));
}
}
Door deze scheiding kun je elk onderdeel apart schalen, monitoren en opnieuw proberen.
De bouwstenen van een event-driven systeem
Elk event-driven PHP-systeem bestaat uit dezelfde fundamentele onderdelen.
1. Event objects
Events zijn vaak simpele data-objecten (value objects). Ze bevatten de minimale informatie die nodig is om erop te reageren.
final class OrderPlaced
{
public function __construct(
public readonly int $orderId,
public readonly int $customerId,
public readonly \DateTimeImmutable $placedAt,
) {}
}
Hoe je deze structuur opzet, sluit mooi aan bij de domain-driven design principes in PHP: je events leven binnen je domein en beschrijven betekenisvolle domeingebeurtenissen.
2. Event dispatcher
De dispatcher is verantwoordelijk voor het publiceren van events. PHP heeft hiervoor een officiële standaard: PSR-14 Event Dispatcher. Frameworks als Laravel en Symfony leveren implementaties out of the box.
$dispatcher->dispatch(new OrderPlaced($order->id, $customer->id, new \DateTimeImmutable()));
3. Listeners en subscribers
Listeners zijn klassen die reageren op specifieke events. In een goed ontworpen systeem staan listeners in hun eigen klasse en volgen ze het Single Responsibility Principle, zie ook de SOLID principes in de praktijk.
4. Message broker
De message broker is het hart van je queue-infrastructuur. Populaire keuzes zijn:
- Redis, snel, laagdrempelig, goed voor kleine tot middelgrote projecten
- RabbitMQ, krachtige routing, uitstekend voor complexere event-driven systemen
- Amazon SQS, volledig managed, geen servers beheren
- Apache Kafka, event streaming op enterprise-schaal met replay-functionaliteit
5. Workers
Workers zijn langlopende PHP-processen die jobs van de queue halen en uitvoeren. Tools als Supervisor of systemd houden ze draaiende.
Praktijkvoorbeeld: een bestelling plaatsen
Laten we een compleet voorbeeld uitwerken. Stel je hebt een webshop waarin een klant een bestelling plaatst.
Stap 1: event definiëren
final class OrderPlaced
{
public function __construct(
public readonly int $orderId,
public readonly int $customerId,
) {}
}
Stap 2: event dispatchen
class OrderService
{
public function __construct(
private OrderRepository $orders,
private EventDispatcherInterface $dispatcher,
) {}
public function place(array $data): Order
{
$order = $this->orders->create($data);
$this->dispatcher->dispatch(
new OrderPlaced($order->id, $order->customerId)
);
return $order;
}
}
Stap 3: listener die jobs op de queue zet
class OrderPlacedListener
{
public function __construct(private QueueInterface $queue) {}
public function handle(OrderPlaced $event): void
{
$this->queue->push(new SendOrderConfirmationEmail($event->orderId));
$this->queue->push(new UpdateInventoryForOrder($event->orderId));
$this->queue->push(new GenerateInvoicePdf($event->orderId));
}
}
Stap 4: job implementeren
class SendOrderConfirmationEmail
{
public function __construct(public readonly int $orderId) {}
public function handle(OrderRepository $orders, Mailer $mailer): void
{
$order = $orders->find($this->orderId);
$mailer->sendOrderConfirmation($order);
}
}
Stap 5: worker draaien
Vanuit de command line start je een worker:
php artisan queue:work --queue=default --tries=3
Of met Symfony Messenger:
php bin/console messenger:consume async -vv
Betrouwbaarheid: wat als er iets misgaat?
Queues introduceren nieuwe uitdagingen. Wat als een worker crasht? Wat als een job faalt? Wat als een event twee keer binnenkomt?
Idempotente handlers
Zorg dat je jobs idempotent zijn: het meerdere keren uitvoeren levert hetzelfde resultaat op. Bijvoorbeeld, controleer of een e-mail al verstuurd is voordat je een tweede verstuurt.
Retries en exponential backoff
De meeste queue-systemen ondersteunen automatische retries met toenemende wachttijden. Faalt een job drie keer op rij? Dan verhuist hij naar een dead letter queue waar je hem handmatig kunt onderzoeken.
Logging en monitoring
Log elk event en elke jobuitvoering. Tools als Laravel Horizon of Symfony Messenger hebben ingebouwde dashboards. Combineer dit met de inzichten uit performance optimalisatie in PHP om bottlenecks te vinden.
Transactional outbox pattern
Wil je absoluut zeker weten dat een event verzonden wordt als een database-transactie succesvol is? Gebruik het transactional outbox pattern: schrijf het event naar een outbox tabel binnen dezelfde transactie en laat een aparte worker die tabel leegtrekken naar de queue.
Event-driven met Laravel of Symfony
In de praktijk bouw je zelden alles from scratch. Twee grote PHP-frameworks leveren uitstekende ondersteuning.
Laravel heeft het Event en Queue systeem nauw geïntegreerd. Met drivers voor Redis, SQS, Beanstalkd en database-queues kun je binnen minuten een event-driven flow opzetten. Zie ook onze Laravel introductie.
Symfony Messenger is een volwassen message bus die events, commands en queries in één bus ondersteunt. Transport naar AMQP, Redis, Doctrine en meer is ingebouwd. De officiële Symfony Messenger documentatie is een aanrader.
Beide frameworks bouwen op gevestigde design patterns in PHP, waaronder het Observer pattern dat aan de basis staat van event-driven denken.
Wanneer wel en wanneer niet?
Event-driven architectuur is krachtig, maar niet altijd de juiste keuze.
Gebruik het wel wanneer:
- Je langzame taken hebt die de gebruiker niet direct hoeft te zien
- Meerdere onderdelen moeten reageren op één gebeurtenis
- Je onafhankelijke services wilt die losgekoppeld zijn
- Pieken in belasting opgevangen moeten worden
Vermijd het wanneer:
- Je applicatie klein is en synchrone calls volstaan
- Je team nog geen ervaring heeft met queues en monitoring
- Directe feedback aan de gebruiker essentieel is (debounce of synchroon blijft dan beter)
De extra complexiteit betaalt zich pas terug als je systeem écht behoefte heeft aan ontkoppeling en schaal.
Monitoring en debugging
Een event-driven systeem is moeilijker te debuggen dan lineaire code. Drie tips:
- Correlation IDs, voeg aan elk event een unieke ID toe die door alle jobs heen wordt meegegeven. Zo kun je een complete flow traceren in je logs.
- Event store, bewaar elk gepubliceerd event in een database. Dit helpt bij debugging en opent de deur naar event sourcing.
- Dashboards, tools als Laravel Horizon, Symfony Messenger Profiler of RabbitMQ Management geven realtime inzicht in wachtrijen en workers.
Veelgestelde vragen
Wat is event-driven PHP met queues?
Event-driven PHP is een architectuur waarbij onderdelen van je applicatie reageren op gebeurtenissen (events) in plaats van elkaar direct aan te roepen. Queues zorgen ervoor dat die events asynchroon verwerkt worden door workers, waardoor je applicatie sneller en schaalbaarder wordt.
Wanneer gebruik je een event-driven aanpak?
Gebruik een event-driven aanpak wanneer onderdelen van je systeem losgekoppeld moeten zijn, of wanneer bepaalde acties traag zijn en niet on-demand uitgevoerd hoeven te worden. Denk aan e-mails versturen, rapportages genereren of externe API's aanroepen na een bestelling.
Wat is het verschil tussen events en jobs?
Een event beschrijft iets dat gebeurd is (bijvoorbeeld OrderPlaced), terwijl een job een concrete taak is die uitgevoerd moet worden (bijvoorbeeld SendOrderConfirmationEmail). Vaak triggert één event meerdere jobs die via een queue worden afgehandeld.
Welke message brokers werken goed met PHP?
Populaire opties zijn RabbitMQ, Redis, Amazon SQS en Kafka. Redis is laagdrempelig en snel voor kleinere projecten, terwijl RabbitMQ en Kafka geschikt zijn voor complexere event-driven architecturen met meerdere consumers.
Hoe zorg je dat events betrouwbaar worden verwerkt?
Gebruik idempotente handlers, retry-mechanismen en een dead letter queue voor mislukte jobs. Monitor je workers actief en log elk event zodat je kunt traceren wat er gebeurd is wanneer iets misgaat.
Tot slot
Event-driven PHP met queues is geen hype, maar een bewezen aanpak om PHP-applicaties schaalbaar en onderhoudbaar te houden. Door events te scheiden van jobs, jobs idempotent te maken en met een solide message broker te werken, bouw je systemen die pieken aankunnen en waar teams parallel aan kunnen werken.
Begin klein: kies één flow in je applicatie (bijvoorbeeld e-mailverzending) en verplaats die naar een queue. Ervaar hoe het voelt om een gebruiker direct een response te geven terwijl het echte werk op de achtergrond doorgaat. Van daaruit bouw je stap voor stap een volwaardige event-driven architectuur uit.