Part of Jardis — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is part of the open-source foundation that generated code runs on.
Domain events for PHP as first-class citizens. A lightweight PSR-14 event dispatcher — built for DDD applications where events drive the communication between layers and contexts. No framework, no overhead, no magic. Just what you need.
- Four classes, zero magic —
EventDispatcher,ListenerProvider,Event,EventCollector - Priority ordering — listeners with higher priority are called first
- Type-hierarchy matching — a listener on an interface catches all implementing events
- Stoppable events — break the listener chain when an event is considered handled
- EventCollector — collect events in the domain layer, dispatch them all at once in the application layer
- PSR-14 compliant — works with any PSR-14 compatible code
- 100% test coverage — no mocks, real execution only
composer require jardisadapter/eventdispatcheruse JardisAdapter\EventDispatcher\Event;
final class OrderCreated extends Event
{
public function __construct(
public readonly string $orderId,
) {
}
}use JardisAdapter\EventDispatcher\EventDispatcher;
use JardisAdapter\EventDispatcher\ListenerProvider;
$provider = new ListenerProvider();
$provider->listen(OrderCreated::class, function (OrderCreated $event): void {
echo "Order {$event->orderId} created!";
});
$dispatcher = new EventDispatcher($provider);
$dispatcher->dispatch(new OrderCreated('ORD-42'));$provider->listen(OrderCreated::class, $sendConfirmation, priority: 10); // first
$provider->listen(OrderCreated::class, $updateInventory, priority: 5); // second
$provider->listen(OrderCreated::class, $logEvent); // last (0)Higher number = higher priority = called first.
$provider->remove(OrderCreated::class, $sendConfirmation);A listener on an interface or parent class catches all events that implement or extend it:
interface PaymentEventInterface {}
final class PaymentReceived extends Event implements PaymentEventInterface {}
final class PaymentFailed extends Event implements PaymentEventInterface {}
// Catches both PaymentReceived AND PaymentFailed
$provider->listen(PaymentEventInterface::class, $paymentAuditor);
// Catches EVERY event that extends Event
$provider->listen(Event::class, $globalLogger);Direct and wildcard listeners are sorted together by priority.
A listener can stop further processing:
$provider->listen(OrderCreated::class, function (OrderCreated $event): void {
if ($event->orderId === 'BLOCKED') {
$event->stopPropagation(); // no further listeners will be called
}
}, priority: 100);
$provider->listen(OrderCreated::class, function (OrderCreated $event): void {
// Only called if stopPropagation() was NOT invoked
});Any event extending Event or implementing StoppableEventInterface supports this automatically.
Collect events in the domain layer, dispatch them later in the application layer:
use JardisAdapter\EventDispatcher\EventCollector;
$collector = new EventCollector();
// In the domain layer — record events
$collector->record(new OrderCreated($orderId));
$collector->record(new InventoryReserved($itemId));
// In the application layer — after the use case completes
$collector->dispatchAll($dispatcher); // dispatches all, clears the listThe collector separates the occurrence of an event (domain) from its distribution (application). Ideal for use cases that produce multiple events.
$collector->count(); // number of collected events
$collector->events(); // read events without dispatching
$collector->clear(); // clear the list without dispatching| Situation | Behavior |
|---|---|
| Listener throws an exception | Propagates unchanged to the caller |
| No listener registered | Event is silently ignored |
| Event already stopped | No listener is called |
No custom exception classes. Errors come from the listeners, not from the dispatcher.
EventDispatcher (implements EventDispatcherInterface)
│
│ dispatch(object $event): object
│ └── iterates listeners, respects StoppableEventInterface
│
└── ListenerProvider (implements ListenerProviderInterface, EventListenerRegistryInterface)
│
├── listen() register listener with priority
├── remove() remove a listener
└── getListenersForEvent()
└── type-hierarchy matching + priority sorting
Event (abstract, implements StoppableEventInterface)
└── stopPropagation() / isPropagationStopped()
EventCollector
└── record() → dispatchAll() / events() / clear() / count()
The dispatcher is the postman — it receives the event and delivers it to all recipients. The listener provider is the address book. The event collector is the mailbox in the domain layer.
| Layer | Responsibility |
|---|---|
| Domain | Defines event classes. Does not dispatch — returns events instead |
| Application | Receives EventDispatcherInterface via injection. Dispatches after use case execution |
| Infrastructure | Registers listeners in the ListenerProvider |
jardiscore/foundation is deleted. ENV bootstrap now lives in jardiscore/kernel's Bootstrap-Packer (Bootstrap\BuildDomainKernelFromEnv), which wires the dispatcher pair via two handlers under core/kernel/src/Bootstrap/Handler/: BuildEventListenerProviderFromEnv builds the shared ListenerProvider, BuildEventDispatcherFromProvider wraps it into the PSR-14 dispatcher.
// Inside a class extending `{Domain}Context`
$dispatcher = $this->resource()->eventDispatcher();
if ($dispatcher !== null) {
$dispatcher->dispatch(new OrderCreated($orderId));
}The same ListenerProvider instance is also reachable via $kernel->eventListenerRegistry(): ?EventListenerRegistryInterface, so generated {Agg}EventRouter scaffolds can register listeners without a separate wiring step.
DomainKernel::eventDispatcher() is typed ?EventDispatcherInterface (core/kernel/src/DomainKernel.php:81) — the return value is the instance or null, there is no third state.
| Return value | Meaning |
|---|---|
EventDispatcher |
Dispatcher active, shared via the DomainKernel |
null |
Adapter not installed, or no listener provider was built |
cp .env.example .env # Once
make install # Install dependencies
make phpunit # Run tests
make phpstan # Static analysis (level 8)
make phpcs # Coding standards (PSR-12)Full documentation, guides, and API reference:
docs.jardis.io/en/adapter/eventdispatcher
MIT License — free for any use, including commercial.
This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:
composer require --dev jardis/dev-skillsMore details: https://docs.jardis.io/en/skills