phariscope / event
To implement events in domain driven design or other styles projects with PHP
Installs: 7 655
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Requires
- php: >=8.1
- psr/event-dispatcher: ^1.0
Requires (Dev)
- infection/extension-installer: 0.1.2
- infection/infection: ^0.27
- phpstan/phpdoc-parser: ^1.20
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10
- squizlabs/php_codesniffer: 3.*
README
composer require phariscope/event
Usage
Steps are:
- Create a domain event (name + past tense verb, example:
AccountCreated
extendsPhariscope\\Event\\Psr14\\Event
). - Dispatch this event.
- Distribute events (manually, or enable immediate distribution).
Somewhere else:
- create a listener for an event (example: class
SendEmailWhenAccountCreatedListener
implementsPhariscope\\Event\\Psr14\\ListenerInterface
) - register the listener; when the event is distributed the listener will handle it and do what it has to do
Sample usage in an aggregate constructor
You SHOULD dispatch a domain event from an aggregate to signal its creation. This helps apply the Single Responsibility Principle: cross‑cutting concerns (emails, projections, integrations) live in listeners rather than inside the aggregate.
<?php namespace App\\Domain\\Account; use Phariscope\\Event\\EventDispatcher; use Phariscope\\Event\\Psr14\\Event; use Phariscope\\Event\\Psr14\\ListenerInterface; // 1) The domain event final class AccountCreated extends Event { public function __construct( public string $accountId, \DateTimeImmutable $occurredOn = new \DateTimeImmutable() ) { parent::__construct($occurredOn); } } // 2) The aggregate that emits the event final class Account { public function __construct(private string $id) { // ... domain invariants and state initialization ... // At the end of construction, dispatch the domain event EventDispatcher::instance()->dispatch(new AccountCreated($this->id)); } } // 3) A listener that reacts to the event final class SendWelcomeEmailListener implements ListenerInterface { public function handle(Event $event): bool { if (!$event instanceof AccountCreated) { return false; } // send email here return true; } public function isSubscribedTo(Event $event): bool { return $event instanceof AccountCreated; } } // 4) Wiring the listener and triggering the flow $dispatcher = EventDispatcher::instance(); $dispatcher->subscribe(new SendWelcomeEmailListener()); // Somewhere in your application flow new Account('acc-123'); // Process the queued events (unless you enabled immediate distribution) $dispatcher->distribute();
Immediate distribution
By default, events dispatched via EventDispatcher::dispatch()
are queued (FIFO) and processed when you call EventDispatcher::distribute()
.
If you want events to be processed immediately upon dispatch, enable immediate distribution:
use Phariscope\Event\EventDispatcher; $dispatcher = EventDispatcher::instance(); $dispatcher->distributeImmediately(); // enables automatic distribute() after each dispatch // You can disable it later if needed $dispatcher->disableImmediateDistribution(); // And you can check the current mode if ($dispatcher->isImmediateDistributionEnabled()) { // ... }
Optional logging
You can plug a PSR-3 logger to observe listener exceptions without breaking the dispatch flow:
use Phariscope\Event\EventDispatcher; use Psr\Log\NullLogger; // or Monolog\Logger $dispatcher = EventDispatcher::instance(); $dispatcher->setLogger(new NullLogger());
Deprecated: the misspelled method distributeImmmediatly()
is still available for backward compatibility but will be removed in a future release. Use distributeImmediately()
instead.
Resetting subscribers (useful in tests)
When using the singleton in tests or long‑running processes, you may need to reset the subscriptions between scenarios:
use Phariscope\Event\EventDispatcher; $dispatcher = EventDispatcher::instance(); // Remove all currently subscribed listeners $dispatcher->clearSubscribers(); // If you also need a fresh instance (clears queue and state) EventDispatcher::tearDown(); $dispatcher = EventDispatcher::instance();
PSR-14 compliance (internal integration)
This package integrates PSR-14 semantics internally while preserving the legacy API:
- You can still use
EventDispatcher
(singleton, queue,distribute()
/distributeImmediately()
). - Listeners remain
Phariscope\Event\Psr14\ListenerInterface
. StoppableEventInterface
is honored: if your event implements it and returnstrue
inisPropagationStopped()
, the dispatcher stops invoking further listeners after the current one.- Exceptions thrown by listeners are swallowed by the legacy dispatcher (as before). Use the optional PSR-3 logger to observe them.
Complete DDD/TDD Examples
For comprehensive examples showing how to use this library in a Domain-Driven Design context with Test-Driven Development, see DDD-TDD-EXAMPLES.md. This documentation covers:
- Domain Layer: Aggregates, domain events, and event testing
- Application Layer: Services with event distribution and testing
- Infrastructure Layer: Controllers, email listeners, and integration tests
- Complete workflow: From HTTP request to domain event to email notification
- Key practices: Event dispatching, testing with SpyListener, and distribution modes
Migration guide: Legacy API → PSR-14 (concepts)
Mapping
- Legacy dispatcher:
Phariscope\Event\EventDispatcher
(queued) → PSR concept: synchronous, immediate dispatch - Legacy listener:
Phariscope\Event\Psr14\ListenerInterface
→ PSR concept:callable(object): void
- Legacy provider:
Phariscope\Event\ListenerProvider
(custom type) → PSR concept: provider returns callables for a given event object - Legacy event type:
Phariscope\Event\Psr14\Event
→ PSR concept: anyobject
(optionally implementStoppableEventInterface
) - Immediate distribution:
distributeImmediately()
→ PSR concept: always immediate by design
Steps
-
Install PSR-14 interfaces (already a dependency of this package):
psr/event-dispatcher
. -
Instantiate the legacy dispatcher as usual (PSR semantics are handled internally at distribution time).
-
Enregistrer vos listeners legacy via
EventDispatcher::subscribe()
comme auparavant. -
Remplacer les appels de distribution:
// Avant (legacy) Phariscope\Event\EventDispatcher::instance()->dispatch($event); Phariscope\Event\EventDispatcher::instance()->distribute(); // Après (sémantique PSR intégrée) $dispatcher->dispatch($event); // synchronously handled when immediate distribution is enabled
-
Propagation stoppable (facultatif): implémentez
Psr\EventDispatcher\StoppableEventInterface
et retourneztrue
dansisPropagationStopped()
pour arrêter l’enchaînement des listeners. -
Exceptions: sous PSR-14, les exceptions d’un listener ne sont pas avalées. Si votre code dépendait de l’ancienne résilience, entourez l’appel
dispatch()
d’untry/catch
ou adaptez vos listeners.
Event immutability
Events in this library are treated as immutable messages. Listeners MUST NOT modify the event instance they receive. If you need to propagate additional information, dispatch a new event.
To contribute to phariscope/Event
Requirements
- docker
- git
Install
- git clone git@github.com:phariscope/Event.git
Unit test
bin/phpunit
Using Test-Driven Development (TDD) principles (thanks to Kent Beck and others), following good practices (thanks to Uncle Bob and others) and the great book 'DDD in PHP' by C. Buenosvinos, C. Soronellas, K. Akbary
Quality
- phpcs PSR12
- phpstan level 9
- coverage 100%
- infection MSI >99%
Quick check with:
./codecheck
Check coverage with:
bin/phpunit --coverage-html var
and view 'var/index.html' with your browser
Check infection with:
bin/infection
and view 'var/infection.html' with your browser