PHPackages                             phpdot/event - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. phpdot/event

ActiveLibrary[Queues &amp; Workers](/categories/queues)

phpdot/event
============

PSR-14 event dispatcher with attribute-based listener discovery, async dispatch, ordering, and persistence abstraction.

v1.0.1(4d ago)08MITPHPPHP &gt;=8.4

Since Apr 4Pushed 4d agoCompare

[ Source](https://github.com/phpdot/event)[ Packagist](https://packagist.org/packages/phpdot/event)[ RSS](/packages/phpdot-event/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (14)Versions (3)Used By (0)

phpdot/event
============

[](#phpdotevent)

PSR-14 event dispatcher with attribute-based listener discovery, async dispatch support, ordering/priority, and persistence abstraction. Zero framework dependencies — PSR interfaces only.

---

Table of Contents
-----------------

[](#table-of-contents)

- [Install](#install)
- [Quick Start](#quick-start)
- [Why This Package](#why-this-package)
- [Architecture](#architecture)
    - [Boot Time vs Runtime](#boot-time-vs-runtime)
    - [Dispatch Pipeline](#dispatch-pipeline)
    - [Package Structure](#package-structure)
- [Events](#events)
    - [Notification Events (Immutable)](#notification-events-immutable)
    - [Enhancement Events (Mutable)](#enhancement-events-mutable)
    - [Stoppable Events](#stoppable-events)
- [Listeners](#listeners)
    - [The #\[Listener\] Attribute](#the-listener-attribute)
    - [Single Event Listener](#single-event-listener)
    - [Multi-Event Listener](#multi-event-listener)
    - [Async Listener](#async-listener)
    - [Ordering](#ordering)
- [Dispatching Events](#dispatching-events)
    - [Basic Dispatch](#basic-dispatch)
    - [Dispatch with Stop Propagation](#dispatch-with-stop-propagation)
    - [Dispatch with Async Handlers](#dispatch-with-async-handlers)
    - [Dispatch with Mixed Sync/Async](#dispatch-with-mixed-syncasync)
- [ListenerProvider](#listenerprovider)
    - [Manual Registration](#manual-registration)
    - [Bulk Loading](#bulk-loading)
    - [Loading from Repository](#loading-from-repository)
    - [Event Class Hierarchy](#event-class-hierarchy)
    - [Querying Listeners](#querying-listeners)
- [Contracts (Interfaces)](#contracts-interfaces)
    - [ListenerDiscoveryInterface](#listenerdiscoveryinterface)
    - [ListenerRepositoryInterface](#listenerrepositoryinterface)
    - [AsyncDispatcherInterface](#asyncdispatcherinterface)
- [Default Implementations](#default-implementations)
    - [InMemoryListenerRepository](#inmemorylistenerrepository)
    - [SyncOnlyDispatcher](#synconlydispatcher)
- [Admin GUI Management](#admin-gui-management)
    - [Enable/Disable Listeners](#enabledisable-listeners)
    - [Reorder Listeners](#reorder-listeners)
    - [Sync After Discovery](#sync-after-discovery)
- [Exception Handling](#exception-handling)
    - [Exception Hierarchy](#exception-hierarchy)
    - [Catching Exceptions](#catching-exceptions)
- [Framework Wiring](#framework-wiring)
- [Comparison](#comparison)
- [API Reference](#api-reference)
    - [Listener Attribute](#listener-attribute-api)
    - [ListenerEntry DTO](#listenerentry-dto-api)
    - [EventDispatcher](#eventdispatcher-api)
    - [ListenerProvider](#listenerprovider-api)
    - [StoppableEvent](#stoppableevent-api)
    - [ListenerDiscoveryInterface](#listenerdiscoveryinterface-api)
    - [ListenerRepositoryInterface](#listenerrepositoryinterface-api)
    - [AsyncDispatcherInterface](#asyncdispatcherinterface-api)
    - [InMemoryListenerRepository](#inmemorylistenerrepository-api)
    - [SyncOnlyDispatcher](#synconlydispatcher-api)
    - [Exceptions](#exceptions-api)
- [License](#license)

---

Install
-------

[](#install)

```
composer require phpdot/event
```

RequirementVersionPHP&gt;= 8.3psr/event-dispatcher^1.0psr/container^2.0psr/log^3.0Zero phpdot dependencies. Zero framework coupling.

---

Quick Start
-----------

[](#quick-start)

```
// 1. Define an event — any PHP class, no base class needed
final readonly class UserRegistered
{
    public function __construct(
        public int $userId,
        public string $email,
    ) {}
}

// 2. Create a handler — the attribute IS the registration
#[Listener(UserRegistered::class, order: 1)]
final class SendWelcomeEmail
{
    public function __construct(private MailerInterface $mailer) {}

    public function __invoke(UserRegistered $event): void
    {
        $this->mailer->send($event->email, 'Welcome!');
    }
}

// 3. Wire and dispatch
$provider = new ListenerProvider();
$provider->addListener(UserRegistered::class, SendWelcomeEmail::class, order: 1);

$dispatcher = new EventDispatcher($provider, $container, $asyncDispatcher, $logger);
$dispatcher->dispatch(new UserRegistered(userId: 1, email: 'omar@example.com'));
```

No central configuration file. No service provider. No YAML. The handler declares what it handles.

---

Why This Package
----------------

[](#why-this-package)

Every PHP event dispatcher forces centralized listener registration:

FrameworkRegistration**Laravel**`EventServiceProvider::$listen` array — every team edits one file**Symfony**YAML tags, compiler passes, or `getSubscribedEvents()`**PHPdot**`#[Listener]` attribute on the handler class — no central filePHPdot inverts the registration. Each handler declares what it handles. Discovery finds all listeners at boot time. The runtime dispatcher uses zero-cost in-memory lookups.

**Additionally:**

- Async support built-in via `AsyncDispatcherInterface` (Laravel has ShouldQueue, but tied to Illuminate)
- Order + Priority correctly separated (order = execution sequence, priority = queue urgency)
- Persistence abstraction for admin GUI management (enable/disable without deploy)
- PSR-14 compliant and replaceable by any PSR-14 implementation

---

Architecture
------------

[](#architecture)

### Boot Time vs Runtime

[](#boot-time-vs-runtime)

```
Boot time (once, cached):
    ListenerDiscoveryInterface scans #[Listener] attributes
        → ListenerProvider stores event→handlers mapping in memory
        → Optionally loads overrides from ListenerRepositoryInterface (DB)

Runtime (every dispatch, zero I/O):
    dispatch(object $event)
        → ListenerProvider→getListenersForEvent($event)  ← in-memory lookup
        → sorted by order
        → for each listener:
            if sync:  resolve from PSR-11 container → call __invoke($event)
            if async: AsyncDispatcherInterface→publishAsync(event, handler, priority)
            if StoppableEvent and stopped: break
        → log via PSR-3 LoggerInterface
        → return event object

```

### Dispatch Pipeline

[](#dispatch-pipeline)

```
EventDispatcher::dispatch(object $event)
    │
    ├── Is StoppableEvent and already stopped? → return immediately
    │
    ├── ListenerProvider::getListenersForEvent($event)
    │   ├── Match exact class
    │   ├── Match parent classes
    │   ├── Match interfaces
    │   └── Sort by order (ascending)
    │
    └── For each ListenerEntry:
        ├── Skip if disabled (enabled: false)
        ├── Check StoppableEvent::isPropagationStopped() → break if true
        │
        ├── If sync:
        │   ├── Container::get($handlerClass)
        │   ├── Validate callable
        │   ├── Call $handler($event)
        │   └── Log via PSR-3 (debug on success, error on failure)
        │
        └── If async:
            ├── AsyncDispatcherInterface::publishAsync($event, $handlerClass, $priority)
            └── Log via PSR-3 (debug on success, error on failure)

```

### Package Structure

[](#package-structure)

```
src/
├── Attribute/
│   └── Listener.php                    # #[Listener] attribute — repeatable, class-target
│
├── EventDispatcher.php                 # PSR-14 dispatcher — sync/async, stop propagation, logging
├── ListenerProvider.php                # PSR-14 provider — in-memory map, class hierarchy matching
│
├── DTO/
│   └── ListenerEntry.php              # Immutable descriptor — event, handler, order, async, priority, enabled
│
├── Contract/
│   ├── ListenerDiscoveryInterface.php  # Scanning abstraction — framework implements
│   ├── ListenerRepositoryInterface.php # Persistence abstraction — framework implements
│   └── AsyncDispatcherInterface.php    # Queue abstraction — framework implements
│
├── Event/
│   └── StoppableEvent.php             # Base class for PSR-14 StoppableEventInterface
│
├── Provider/
│   ├── InMemoryListenerRepository.php  # Default — no DB needed
│   └── SyncOnlyDispatcher.php          # Default — runs async handlers synchronously
│
└── Exception/
    ├── EventException.php              # Base (extends RuntimeException)
    ├── ListenerException.php           # Handler resolution/execution failure
    └── AsyncDispatchException.php      # Queue publishing failure

```

13 source files. 812 lines.

---

Events
------

[](#events)

Events are plain PHP objects. No base class required. No interface. No trait.

### Notification Events (Immutable)

[](#notification-events-immutable)

One-way signal: "something happened." Listeners react but don't modify the event.

```
final readonly class UserRegistered
{
    public function __construct(
        public int $userId,
        public string $email,
        public DateTimeImmutable $registeredAt,
    ) {}
}

final readonly class OrderPlaced
{
    public function __construct(
        public int $orderId,
        public int $userId,
        public float $total,
    ) {}
}
```

### Enhancement Events (Mutable)

[](#enhancement-events-mutable)

Two-way signal: "modify this before I use it." Listeners enrich the event.

```
final class ResponseCreated
{
    /** @var list */
    public array $headers = [];

    public function __construct(
        public readonly Response $response,
    ) {}

    public function addHeader(string $header): void
    {
        $this->headers[] = $header;
    }
}
```

### Stoppable Events

[](#stoppable-events)

First handler that can handle it wins. Extend `StoppableEvent` and call `stopPropagation()`.

```
use PHPdot\Event\Event\StoppableEvent;

final class RouteMatched extends StoppableEvent
{
    public ?Route $route = null;

    public function __construct(
        public readonly string $path,
    ) {}
}

// First listener that matches stops propagation
#[Listener(RouteMatched::class, order: 1)]
final class ApiRouteResolver
{
    public function __invoke(RouteMatched $event): void
    {
        if (str_starts_with($event->path, '/api/')) {
            $event->route = $this->resolveApiRoute($event->path);
            $event->stopPropagation();
        }
    }
}

#[Listener(RouteMatched::class, order: 2)]
final class WebRouteResolver
{
    public function __invoke(RouteMatched $event): void
    {
        // Only reached if API resolver didn't match
        $event->route = $this->resolveWebRoute($event->path);
    }
}
```

Events that don't need stopping are plain objects — no StoppableEvent inheritance needed.

---

Listeners
---------

[](#listeners)

### The #\[Listener\] Attribute

[](#the-listener-attribute)

```
use PHPdot\Event\Attribute\Listener;

#[Listener(
    event: UserRegistered::class,  // required — event class to listen for
    order: 1,                       // execution sequence (lower = first, default 0)
    async: false,                   // sync (default) or queue
    priority: 0,                    // queue priority for async (0-10, higher = urgent)
)]
```

The attribute is `IS_REPEATABLE` — one handler can listen to multiple events.

### Single Event Listener

[](#single-event-listener)

```
#[Listener(UserRegistered::class)]
final class SendWelcomeEmail
{
    public function __construct(
        private readonly MailerInterface $mailer,
    ) {}

    public function __invoke(UserRegistered $event): void
    {
        $this->mailer->send($event->email, 'Welcome!');
    }
}
```

Handlers must be callable — implement `__invoke()`. Resolved from the PSR-11 container (constructor injection works).

### Multi-Event Listener

[](#multi-event-listener)

```
#[Listener(UserRegistered::class, order: 1)]
#[Listener(UserUpdated::class, order: 1)]
final class UpdateSearchIndex
{
    public function __invoke(UserRegistered|UserUpdated $event): void
    {
        $this->search->index('users', $event->userId);
    }
}
```

### Async Listener

[](#async-listener)

```
#[Listener(OrderPlaced::class, order: 3, async: true, priority: 5)]
final class SendOrderConfirmation
{
    public function __invoke(OrderPlaced $event): void
    {
        $this->mailer->send($event->userId, 'order.confirmation');
    }
}
```

Async listeners are published to the queue via `AsyncDispatcherInterface`. They don't block the dispatch call. The handler runs later when a queue worker consumes the message.

### Ordering

[](#ordering)

Order controls **execution sequence** within a single event. Lower numbers run first.

```
#[Listener(OrderPlaced::class, order: 1)]  // runs 1st
final class ValidateOrder { ... }

#[Listener(OrderPlaced::class, order: 2)]  // runs 2nd
final class ChargePayment { ... }

#[Listener(OrderPlaced::class, order: 3)]  // runs 3rd
final class ReserveInventory { ... }

#[Listener(OrderPlaced::class, order: 4, async: true, priority: 5)]  // queued 4th
final class SendConfirmation { ... }

#[Listener(OrderPlaced::class, order: 5, async: true, priority: 1)]  // queued 5th, lower queue priority
final class TrackAnalytics { ... }
```

**Order** and **priority** are separate concerns:

- `order` — when this listener runs relative to others for the same event (sync and async)
- `priority` — how urgently the queue should process this async listener (async only)

---

Dispatching Events
------------------

[](#dispatching-events)

### Basic Dispatch

[](#basic-dispatch)

```
use Psr\EventDispatcher\EventDispatcherInterface;

final class OrderService
{
    public function __construct(
        private readonly EventDispatcherInterface $dispatcher,
    ) {}

    public function place(int $userId, Cart $cart): Order
    {
        $order = $this->createOrder($userId, $cart);

        $this->dispatcher->dispatch(new OrderPlaced(
            orderId: $order->id,
            userId: $userId,
            total: $cart->total(),
        ));

        return $order;
    }
}
```

The emitter depends on PSR-14 `EventDispatcherInterface` — knows nothing about handlers.

### Dispatch with Stop Propagation

[](#dispatch-with-stop-propagation)

```
$event = new RouteMatched('/api/users');
$dispatcher->dispatch($event);

// $event->route is set by the first resolver that matched
if ($event->route !== null) {
    $this->executeRoute($event->route);
}
```

If the event implements `StoppableEventInterface` and `isPropagationStopped()` returns true, remaining listeners are skipped — including async ones.

### Dispatch with Async Handlers

[](#dispatch-with-async-handlers)

```
// Async handlers are published to the queue — dispatch returns immediately
$dispatcher->dispatch(new OrderPlaced(42, 1, 99.99));
// ChargePayment (sync) ran inline
// ReserveInventory (sync) ran inline
// SendConfirmation (async) → published to queue → returns immediately
// TrackAnalytics (async) → published to queue → returns immediately
```

### Dispatch with Mixed Sync/Async

[](#dispatch-with-mixed-syncasync)

Sync handlers block. Async handlers return immediately. Order is respected across both.

```
dispatch(OrderPlaced)
    → order 1: ValidateOrder    (sync)  → container.get() → __invoke() → done
    → order 2: ChargePayment    (sync)  → container.get() → __invoke() → done
    → order 3: ReserveInventory (sync)  → container.get() → __invoke() → done
    → order 4: SendConfirmation (async) → queue.publish(priority: 5) → returns immediately
    → order 5: TrackAnalytics   (async) → queue.publish(priority: 1) → returns immediately
    → return event

```

---

ListenerProvider
----------------

[](#listenerprovider)

The provider manages the in-memory event→handlers mapping. Implements PSR-14 `ListenerProviderInterface`.

### Manual Registration

[](#manual-registration)

```
$provider = new ListenerProvider();

$provider->addListener(
    eventClass: UserRegistered::class,
    handlerClass: SendWelcomeEmail::class,
    order: 1,
    async: false,
    priority: 0,
);

$provider->addListener(
    eventClass: UserRegistered::class,
    handlerClass: SyncToMailchimp::class,
    order: 2,
    async: true,
    priority: 5,
);
```

### Bulk Loading

[](#bulk-loading)

```
use PHPdot\Event\DTO\ListenerEntry;

$provider->load([
    new ListenerEntry(UserRegistered::class, SendWelcomeEmail::class, order: 1),
    new ListenerEntry(UserRegistered::class, SyncToMailchimp::class, order: 2, async: true, priority: 5),
    new ListenerEntry(OrderPlaced::class, ChargePayment::class, order: 1),
]);
```

### Loading from Repository

[](#loading-from-repository)

```
// Load DB overrides — merges with existing entries
$provider->loadFromRepository($repository);
```

Repository entries override existing entries with the same event+handler pair. New entries are added. Used for admin GUI management.

### Event Class Hierarchy

[](#event-class-hierarchy)

Listeners registered on a parent class or interface are triggered by subclass events:

```
// Listener on parent class
$provider->addListener(BaseUserEvent::class, AuditLogger::class);

// These events all trigger AuditLogger:
$dispatcher->dispatch(new UserRegistered(...));  // extends BaseUserEvent
$dispatcher->dispatch(new UserUpdated(...));     // extends BaseUserEvent
$dispatcher->dispatch(new UserDeleted(...));     // extends BaseUserEvent

// Listener on interface
$provider->addListener(AuditableInterface::class, AuditLogger::class);

// Any event implementing AuditableInterface triggers AuditLogger
```

Matching order: exact class → parent classes → interfaces. All sorted by order.

### Querying Listeners

[](#querying-listeners)

```
$provider->hasListeners(UserRegistered::class);  // bool
$provider->getAll();                               // array
$provider->removeListeners(UserRegistered::class); // remove all for this event
$provider->clear();                                // remove everything
```

---

Contracts (Interfaces)
----------------------

[](#contracts-interfaces)

Three interfaces that the framework implements. The event package ships with default in-memory implementations.

### ListenerDiscoveryInterface

[](#listenerdiscoveryinterface)

Scans the codebase for `#[Listener]` attributes. Framework implements using its attribute scanner.

```
interface ListenerDiscoveryInterface
{
    /** @return list */
    public function discover(): array;
}
```

### ListenerRepositoryInterface

[](#listenerrepositoryinterface)

Persists listener mappings for admin GUI management. Framework implements using its database layer.

```
interface ListenerRepositoryInterface
{
    /** @return list */
    public function getAll(): array;

    /** @return list */
    public function getByEvent(string $eventClass): array;

    public function save(ListenerEntry $entry): void;
    public function setEnabled(string $eventClass, string $handlerClass, bool $enabled): void;
    public function setOrder(string $eventClass, string $handlerClass, int $order): void;
    public function delete(string $eventClass, string $handlerClass): void;

    /** @param list $discovered */
    public function sync(array $discovered): void;
}
```

### AsyncDispatcherInterface

[](#asyncdispatcherinterface)

Publishes events to a message queue. Framework implements using its queue layer.

```
interface AsyncDispatcherInterface
{
    public function publishAsync(object $event, string $handlerClass, int $priority = 0): void;
}
```

---

Default Implementations
-----------------------

[](#default-implementations)

### InMemoryListenerRepository

[](#inmemorylistenerrepository)

No database needed. Full CRUD. Preserves admin overrides on sync.

```
use PHPdot\Event\Provider\InMemoryListenerRepository;

$repo = new InMemoryListenerRepository();

$repo->save(new ListenerEntry(UserRegistered::class, SendEmail::class, order: 1));
$repo->setEnabled(UserRegistered::class, SendEmail::class, false);
$repo->setOrder(UserRegistered::class, SendEmail::class, 5);
$repo->delete(UserRegistered::class, SendEmail::class);
$repo->getAll();
$repo->getByEvent(UserRegistered::class);
$repo->sync($discoveredEntries);  // merge, preserve overrides, remove stale
```

### SyncOnlyDispatcher

[](#synconlydispatcher)

Runs async handlers synchronously. Useful for development, testing, and simple deployments where no message queue is configured.

```
use PHPdot\Event\Provider\SyncOnlyDispatcher;

$async = new SyncOnlyDispatcher($container);

// "Async" handlers just run inline
$async->publishAsync($event, SendEmail::class, priority: 5);
// SendEmail::__invoke($event) called synchronously
```

---

Admin GUI Management
--------------------

[](#admin-gui-management)

The `ListenerRepositoryInterface` enables runtime listener management without code deploy.

### Enable/Disable Listeners

[](#enabledisable-listeners)

```
// Disable a listener — it will be skipped during dispatch
$repository->setEnabled(UserRegistered::class, SendWelcomeEmail::class, false);

// Re-enable
$repository->setEnabled(UserRegistered::class, SendWelcomeEmail::class, true);
```

### Reorder Listeners

[](#reorder-listeners)

```
// Change execution order without code change
$repository->setOrder(UserRegistered::class, SendWelcomeEmail::class, 10);
$repository->setOrder(UserRegistered::class, SyncToMailchimp::class, 1);  // now runs first
```

### Sync After Discovery

[](#sync-after-discovery)

When the application boots, newly discovered listeners are merged with stored ones. Admin overrides (enabled/disabled, reordered) are preserved. Handlers removed from code are cleaned up.

```
$discovered = $discovery->discover();
$repository->sync($discovered);

$provider = new ListenerProvider();
$provider->load($discovered);
$provider->loadFromRepository($repository);  // applies DB overrides
```

---

Exception Handling
------------------

[](#exception-handling)

### Exception Hierarchy

[](#exception-hierarchy)

```
EventException (extends RuntimeException)
├── ListenerException            — handler resolution or execution failure
│   ├── getHandlerClass(): string
│   └── getEventClass(): string
└── AsyncDispatchException       — queue publishing failure
    ├── getHandlerClass(): string
    └── getEventClass(): string

```

All exceptions carry the original cause as `getPrevious()`.

### Catching Exceptions

[](#catching-exceptions)

```
use PHPdot\Event\Exception\ListenerException;
use PHPdot\Event\Exception\AsyncDispatchException;
use PHPdot\Event\Exception\EventException;

try {
    $dispatcher->dispatch(new OrderPlaced(...));
} catch (ListenerException $e) {
    // Sync handler failed
    $e->getHandlerClass();  // 'App\Listener\ChargePayment'
    $e->getEventClass();    // 'App\Event\OrderPlaced'
    $e->getPrevious();      // original exception
} catch (AsyncDispatchException $e) {
    // Queue publishing failed
    $e->getHandlerClass();  // 'App\Listener\SendConfirmation'
    $e->getEventClass();    // 'App\Event\OrderPlaced'
} catch (EventException $e) {
    // Catch-all
}
```

A `ListenerException` is also thrown when a handler resolved from the container is not callable.

---

Framework Wiring
----------------

[](#framework-wiring)

How phpdot/dot (or any framework) wires this package at boot time:

```
// 1. Discover #[Listener] attributes
$discovery = new AttributeListenerDiscovery($attributeScanner, $paths);
$entries = $discovery->discover();

// 2. Build the provider
$provider = new ListenerProvider();
$provider->load($entries);

// 3. Optionally load DB overrides
$repository = new DatabaseListenerRepository($db);
$provider->loadFromRepository($repository);

// 4. Wire the async dispatcher
$asyncDispatcher = new QueueAsyncDispatcher($queue, $serializer);

// 5. Create the event dispatcher
$dispatcher = new EventDispatcher(
    provider: $provider,
    container: $container,
    async: $asyncDispatcher,
    logger: $logger,
);

// 6. Register as PSR-14
$container->set(EventDispatcherInterface::class, $dispatcher);
```

The framework implementations (`AttributeListenerDiscovery`, `DatabaseListenerRepository`, `QueueAsyncDispatcher`) live in the framework, not in this package.

---

Comparison
----------

[](#comparison)

FeaturePHPdotSymfonyLaravel**Registration**`#[Listener]` attributeYAML/tags/subscriberEventServiceProvider array**Central config file**Noneservices.yamlEventServiceProvider**Auto-discovery**Via ListenerDiscoveryInterfaceCompiler passhandle() type-hint**Admin GUI manageable**ListenerRepositoryInterfaceNoNo**Enable/disable without deploy**YesNoNo**Events**Any PHP objectExtends Event (optional)Any class**Type safety**Class-based identityString or FQCNFQCN**Stop propagation**StoppableEventEvent::stopPropagation()return false**Async dispatch**AsyncDispatcherInterfaceMessenger (separate)ShouldQueue**Order control**`order` paramPriority (numeric)Registration order**Queue priority**`priority` paramN/A$queue property**PSR-14 compliant**YesYesNo**Standalone**YesYesNo (illuminate/\*)**Replaceable**Any PSR-14 implAny PSR-14 implNo---

API Reference
-------------

[](#api-reference)

### Listener Attribute API

[](#listener-attribute-api)

```
#[Attribute(TARGET_CLASS | IS_REPEATABLE)]
final readonly class Listener

__construct(
    public string $event,          // event class name
    public int    $order    = 0,   // execution order (lower = first)
    public bool   $async    = false, // run via queue
    public int    $priority = 0,   // queue priority (0-10)
)

```

### ListenerEntry DTO API

[](#listenerentry-dto-api)

```
final readonly class ListenerEntry

__construct(
    public string $eventClass,
    public string $handlerClass,
    public int    $order    = 0,
    public bool   $async    = false,
    public int    $priority = 0,
    public bool   $enabled  = true,
)

```

### EventDispatcher API

[](#eventdispatcher-api)

```
final class EventDispatcher implements EventDispatcherInterface

__construct(
    ListenerProvider         $provider,
    ContainerInterface       $container,
    AsyncDispatcherInterface $async,
    LoggerInterface          $logger,
)

dispatch(object $event): object

```

### ListenerProvider API

[](#listenerprovider-api)

```
final class ListenerProvider implements ListenerProviderInterface

getListenersForEvent(object $event): iterable
addListener(string $eventClass, string $handlerClass, int $order = 0, bool $async = false, int $priority = 0): void
load(list $entries): void
loadFromRepository(ListenerRepositoryInterface $repository): void
getAll(): array
hasListeners(string $eventClass): bool
removeListeners(string $eventClass): void
clear(): void

```

### StoppableEvent API

[](#stoppableevent-api)

```
abstract class StoppableEvent implements StoppableEventInterface

isPropagationStopped(): bool
stopPropagation(): void

```

### ListenerDiscoveryInterface API

[](#listenerdiscoveryinterface-api)

```
interface ListenerDiscoveryInterface

discover(): list

```

### ListenerRepositoryInterface API

[](#listenerrepositoryinterface-api)

```
interface ListenerRepositoryInterface

getAll(): list
getByEvent(string $eventClass): list
save(ListenerEntry $entry): void
setEnabled(string $eventClass, string $handlerClass, bool $enabled): void
setOrder(string $eventClass, string $handlerClass, int $order): void
delete(string $eventClass, string $handlerClass): void
sync(list $discovered): void

```

### AsyncDispatcherInterface API

[](#asyncdispatcherinterface-api)

```
interface AsyncDispatcherInterface

publishAsync(object $event, string $handlerClass, int $priority = 0): void

```

### InMemoryListenerRepository API

[](#inmemorylistenerrepository-api)

```
final class InMemoryListenerRepository implements ListenerRepositoryInterface

getAll(): list
getByEvent(string $eventClass): list
save(ListenerEntry $entry): void
setEnabled(string $eventClass, string $handlerClass, bool $enabled): void
setOrder(string $eventClass, string $handlerClass, int $order): void
delete(string $eventClass, string $handlerClass): void
sync(list $discovered): void

```

### SyncOnlyDispatcher API

[](#synconlydispatcher-api)

```
final class SyncOnlyDispatcher implements AsyncDispatcherInterface

__construct(ContainerInterface $container)
publishAsync(object $event, string $handlerClass, int $priority = 0): void

```

### Exceptions API

[](#exceptions-api)

```
EventException (extends RuntimeException)

ListenerException (extends EventException)
    __construct(string $message, string $handlerClass, string $eventClass, int $code = 0, ?Throwable $previous = null)
    getHandlerClass(): string
    getEventClass(): string

AsyncDispatchException (extends EventException)
    __construct(string $message, string $handlerClass, string $eventClass, int $code = 0, ?Throwable $previous = null)
    getHandlerClass(): string
    getEventClass(): string

```

---

License
-------

[](#license)

MIT

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance99

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~87 days

Total

2

Last Release

4d ago

PHP version history (2 changes)v1.0.0PHP &gt;=8.3

v1.0.1PHP &gt;=8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/62e82421bda4b5d6ba9a47ba6d88caca060dcd0d1a2862f351f3a97657385db0?d=identicon)[phpdot](/maintainers/phpdot)

---

Top Contributors

[![phpdot](https://avatars.githubusercontent.com/u/252500?v=4)](https://github.com/phpdot "phpdot (4 commits)")

---

Tags

eventasyncpsr-14listenerdispatcherattribute

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/phpdot-event/health.svg)

```
[![Health](https://phpackages.com/badges/phpdot-event/health.svg)](https://phpackages.com/packages/phpdot-event)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.2M2.2k](/packages/symfony-symfony)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6942.5M421](/packages/drupal-core-recommended)[symfony/mailer

Helps sending emails

1.6k409.1M1.4k](/packages/symfony-mailer)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

564576.7k53](/packages/ecotone-ecotone)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
