What's an Event?
We only mean the first one
Component A wants to act when Component B acts
Almost everyone does Mediated Observer
$listener = new PayforPizza();
$dispatcher->addListener('pizza.arrived', [$listener, 'onPizzaArrived']);
// ...
$pizzaHere = new PizzaArrivedEvent();
$dispatcher->dispatch('pizza.arrived', $pizzaHere);
if ($pizzaHere->wasAnchovies()) {
throw new UpException();
}
$pizzaHere = new PizzaArrivedEvent();
event($pizzaHere);
// or
Event::fire(new TestEvent());
$event = $emitter->emit('pizza.arrived');
// or
$pizzaHere = new class implements EventInterface { ... }
$pizzaHere = $emitter->emit($pizzaHere);
if ($pizzaHere->wasAnchovies()) {
throw new UpException();
}
// or
$events = $emitter->emitBatch([$pizza, $pizza]);
$eventManager->triggerEvent(new PizzaArrivedEvent());
// or
$eventManager->triggerEventUntil(function ($returnValue) {
if ($returnValue instanceof Payment && $returnValue->paymentReceived()) {
return true;
}
return false;
}, new PizzaArrivedEvent());
function modulename_pizza_arrived($type) { ... }
$result = module_invoke_all('pizza_arrived', 'anchovies');
// $result is an array, but can also pass by reference, sort of.
$signalSlotDispatcher->connect(
PizzaDelivery::class,
'SIGNAL_afterarrival',
MyListener::class,
'eatThePizzaMethod'
);
$signalSlotDispatcher->dispatch(
__CLASS__, 'pizzaArrived', [$extensionKey, $this]
);
So which do I use?
As a library author I want to target one extension mechanism So that people can use my library with any framework
Bec Simensen, https://ninjagrl.com/artwork/fig
I did a thing! If you care...
Please change this thing before I use it.
Give me all your things!
First to use it wins
interface EventDispatcherInterface {
/**
* @return object
* The Event that was passed, now modified by listeners.
*/
public function dispatch(object $event);
}
interface ListenerProviderInterface {
public function getListenersForEvent(object $event) : iterable;
}
interface StoppableEventInterface {
public function isPropagationStopped() : bool;
}
$provider = new Provider();
// ...
$dispatcher = new Dispatcher($provider);
$pizzaArrived = $dispatcher->dispatch(new PizzaArrived());
class Dispatcher implements DispatcherInterface {
// Constructor goes here.
public function dispatch(object $event) {
foreach ($this->provider->getListenersForEvent($event) as $listener) {
if ($event instanceof StoppableEventInterface
&& $event->isPropagationStopped()) {
break;
}
$listener($event);
}
return $event;
}
}
("Unstoppable Events" is my new band name...)
Dispatchers are the API to calling libraries
Providers are the API to frameworks
Can map Events to Listeners based on...
Can order Listeners based on...
"Listener" is any PHP callable
So how do Listeners get into the Provider?
$provider = new OrderedListenerProvider();
// Any order.
$provider->addListener(function(PizzaArrived $event) {
log($event->hasAnchovies());
});
// Priority ordering.
$key = $provider->addListener([$listener, 'pizzaHere'], 5);
// Relative ordering.
$provider->addListenerBefore($key, 'my_listener_function');
Events map to Listeners by reflection!
$provider = new OrderedListenerProvider($PSR_11_Container);
$provider->addListenerService(
'service_name', 'handlePizza', PizzaArrived::class, 5
);
// (Also before/after support)
class MySubscriber implements SubscriberInterface {
// This method will become a Listener for PizzaArrived.
public function onPizzaArrived(PizzaArrived $event) : void { /*...*/ }
#[ListenerPriority(10)]
public function ifPizzaWrong(WrongPizzaDelivered $event) : void { /*...*/ }
// This method is registered explicitly below.
public function ifPizzaLate(PizzaArrivedLate $event) : void { /*...*/ }
public static function registerListeners(ListenerProxy $proxy): void
{
// Give this Listener a higher-than-default priority.
$a = $proxy->addListener('ifPizzaLate', 10);
}
}
$provider->addSubscriber('service_name', MySubscriber::class);
(Most of this gets setup in your Container config.)
$builder = new ProviderBuilder();
$builder->addSubscriberService(PizzaSubscriber::class, 'pizza_subscriber');
$compiler = new ProviderCompiler();
// Write the generated compiler out to a file.
$filename = '/path/to/compiled/code/provider.php';
$out = fopen($filename, 'w');
$compiler->compile($builder, $out, 'CompiledProvider', 'My\\App');
fclose($out);
include($filename); // Or autoload it.
$provider = new My\App\CompiledProvider($container);
$dispatcher = new WhateverDispatcher($provider);
composer.json
{
"extra": {
"psr-14": {
"default": "src/Listeners"
}
}
}
setup.php
$provider = new Bmack\KartComposerPlugin\ComposerReflectionListenerProvider();
Any method on a class in that directory is now a Listener.
You're welcome.
Or write your own
Or use your framework's
(If yours isn't here, feature request time!)
Packagist: 41,127,008 Installs, 349 Dependents
Access permitted iff...
class AccessCheck implements StoppableEventInterface {
protected Document $doc;
protected ?bool $allow = null;
public function __construct(Document $doc) { $this->doc = $doc; }
public function document(): Document { return $this->doc; }
public function allow(): void { $this->allow = true; }
public function deny(): void { $this->allow = false; }
public function allowed(): bool {
return $this->allow === true;
}
public function isPropagationStopped(): bool {
return $this->allow === false;
}
}
class CreateAccessCheck extends AccessCheck {}
class ReadAccessCheck extends AccessCheck {}
class UpdateAccessCheck extends AccessCheck {}
class DeleteAccessCheck extends AccessCheck {}
$provider = new OrderedListenerProvider();
$dispatcher = new Dispatcher($provider);
$user = get_current_user_somehow();
$provider->addListener(function (AccessCheck $event) use ($user) {
if ($user->isGuest()) {
$event->deny();
}
});
// Note specific class.
$provider->addListener(function (UpdateAccessCheck $event) use ($user) {
$isOwnDocument = $event->document()->owner() == $user->id();
if ($isOwnDocument && $user->hasPermission('edit own documents')) {
$event->allow();
}
});
$allow = $dispatcher->dispatch(new UpdateAccessCheck($document))->allowed();
if (!$allow) {
// Show an error message or something.
}
AccessCheck
is the API!
Works with any Dispatcher or Provider!
class AccessCheckerProvider implements ListenerProviderInterface {
public function addVoter(callable $voter, bool $canDeny = false) { ... }
public function getListenersForEvent(object $event): iterable { ... }
}
Does that mean I need multiple dispatchers?
class AggregateProvider implements ListenerProviderInterface {
// ...
public function addProvider(ListenerProviderInterface $p): void {
$this->providers[] = $provider;
}
public function getListenersForEvent(object $event): iterable {
foreach ($this->providers as $provider) {
yield from $provider->getListenersForEvent($event);
}
}
}
Provider concatenation
class DelegatingProvider implements ListenerProviderInterface {
protected $providers = [
PizzaArrived::class => [$provider1, $provider2],
TacosArrived::class => [$provider3, $provider4],
AccessCheck::class => [$accessCheckProvider],
];
// ...
public function getListenersForEvent(object $event): iterable {
foreach ($this->providers as $type => $providers) {
if ($event instanceof $type) {
foreach ($providers as $provider) {
yield from $provider->getListenersForEvent($event);
}
return;
}
}
yield from $this->defaultProvider->getListenersForEvent($event);
}
}
Provider branching
$compiledProvider = new MyCompiledProvider();
$orderedProvider = new OrderedListenerProvider();
$defaultProvider = new AggregateProvider();
$defaultProvider->addProvider($compiledProvider);
$defaultProvider->addProvider($orderedProvider);
$accessCheckProvider = new AccessCheckProvider();
$delegatingProvider = new DelegatingProvider($defaultProvider);
$delegatingProvider->addProvider($accessCheckProvider, [AccessCheck::class]);
$dispatcher = Dispatcher($delegatingProvider);
This Event will be slow, and I don't care about the result.
You don't know that.
Only the Listener author knows what's safe to defer!
I want to send an email on save
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('doc_notifications', false, false, false, false);
$provider->addListener(function (DocumentSaved $event) use ($channel) {
$details = [
'document_id' => $event->getSubject()->id(),
];
$message = new AMQPMessage(serialize($details));
$channel->basic_publish($message, '', 'doc_notifications');
});
class QueueableProvider implements ListenerProviderInterface {
protected $listeners = [];
public function __construct(
protected ContainerInterface $container,
protected AMQPChannel $channel = null,
) {
if ($this->channel) {
$this->channel->queue_declare('events', false, false, false, false);
}
}
// ...
}
public function addListenerService(
string $service, string $method, string $type, bool $queue): void {
if ($queue && $this->channel) {
$listener = $this->makeListenerForQueue($service, $method);
}
else {
$listener = $this->makeListenerForService($service, $method);
}
$this->listeners[] = [
'listener' => $listener,
'type' => $type,
];
}
class QueueableProvider implements ListenerProviderInterface {
// ...
protected function makeListenerForService(
string $service,
string $method,
) : callable {
$container = $this->container;
return fn(object $event) => $container->get($service)->$method($event);
}
}
protected function makeListenerForQueue(
string $service, string $method) : callable {
$channel = $this->channel;
return function (object $event) use ($service, $method, $channel) : void {
$details = [
'service' => $service,
'method' => $method,
'event' => $event,
];
$message = AMQPMessage(serialize($details));
$channel->basic_publish($message, '', 'events');
};
}
class QueueableProvider implements ListenerProviderInterface {
// ...
public function getListenersForEvent(object $event): iterable {
foreach ($this->listeners as $listener) {
if ($event instanceof $listener['type']) {
yield $listener['listener'];
}
}
}
}
function queue_runner(AMQPChannel $channel, ContainerInterface $container) {
$worker = function (AMQPMessage $message) use ($container) {
$details = unserialize($message->body);
$listener = $container->get($details['service'])->$details['method'];
$listener($details['event']));
};
$channel->queue_declare('events', false, false, false, false);
$channel->basic_consume('events', '', false, true, false, false, $worker);
while (count($channel->callbacks)) {
$channel->wait();
}
}
(Too long; checked email)
Interoperability++
composer require psr/event-dispatcher