icanboogie / event
Provides an event API
Installs: 8 583
Dependents: 6
Suggesters: 1
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- php: >=7.2
- icanboogie/accessor: ^2.0|^3.0|^4.0
- icanboogie/inflector: ^1.3|^2.0
Requires (Dev)
- phpunit/phpunit: ^8.5
README
The icanboogie/event allows you to provide hooks which other developers can attach to, to be notified when certain events occur inside the application and take action.
Inside ICanBoogie, events are often used to alter initial parameters, take action before/after an operation is processed or when it fails, take action before/after a request is dispatched or to rescue an exception.
Installation
composer require icanboogie/event
Feature highlights
- Easily implementable.
- Events are typed.
- Events usually have a target object, but simpler event types can also be emitted.
- Event hooks are attached to classes rather than objects, and they are inherited.
- Event hooks can be attached to a finish chain that is executed after the event hooks chain.
- Execution of the event chain can be stopped.
A twist on the Observer pattern
The pattern used by the API is similar to the Observer pattern, although instead of attaching event hooks to objects they are attached to their class. When an event is fired upon a target object, the hierarchy of its class is used to filter event hooks.
Consider the following class hierarchy:
ICanBoogie\Operation
└─ ICanBoogie\Module\Operation\SaveOperation
└─ Icybee\Modules\Node\Operation\SaveOperation
└─ Icybee\Modules\Content\Operation\SaveOperation
└─ Icybee\Modules\News\Operation\SaveOperation
When a ProcessEvent
is emitted with a …\News\Operation\SaveOperation
instance, all event hooks
attached to the classes for this event are called, starting from the event hooks attached to the
instance class (…\News\Operation\SaveOperation
) all the way up to those attached to its root
class.
Thus, event hooks attached to the …\Node\Operation\SaveOperation
class are called when a
ProcessEvent
event is fired with …\News\Operation\SaveOperation
instance. One could consider
that event hooks are inherited.
Getting started
To be emitted, events need an event collection, which holds event hooks. Because a new event collection is created for you when required, you don't need to set one up yourself. Still, you might want to do so if you have a bunch of event hooks that you need to attach while creating the event collection. To do so, you need to define a provider that returns your event collection when required.
The following example demonstrates how to set up a provider that instantiates an event collection with event hooks provided by an app configuration:
<?php namespace ICanBoogie; /* @var Application $app */ EventCollectionProvider::define(function() use ($app) { static $collection; return $collection ??= new EventCollection($app->configs['event']); }); # Getting the event collection $events = EventCollectionProvider::provide(); # or $events = get_events();
Typed events
Events are subclasses of the Event class.
The following code demonstrates how a ProcessEvent
class may be defined:
<?php namespace ICanBoogie\Operation; use ICanBoogie\Event; use ICanBoogie\HTTP\Request; use ICanBoogie\HTTP\Response; use ICanBoogie\Operation; class ProcessEvent extends Event { /** * Reference to the response result property. */ public mixed $result; public function __construct( Operation $target, public readonly Request $request, public readonly Response $response, mixed &$result ) { $this->result = &$result; parent::__construct($target); } }
Event types
If an event has a target, the event is obtained using the for()
method and the target class or
object. If an event doesn't have a target, the event type is the event class.
Namespacing and naming
Event classes should be defined in a namespace unique to their target object. Events targeting
ICanBoogie\Operation
instances should be defined in the ICanBoogie\Operation
namespace.
Firing events
Events are fired with the emit()
function.
<?php namespace ICanBoogie; /* @var Event $event */ emit($event);
Attaching event hooks
Event hooks are attached using the attach()
method of an event collection. The attach()
method
is smart enough to create the event type from the parameter types. This works with any callable:
closure, invokable objects, static class methods, functions.
The following example demonstrates how a closure may be attached to a BeforeProcessEvent
event.
<?php namespace ICanBoogie $detach = $events->attach(function(Operation\BeforeProcessEvent $event, Operation $target) { // … }); # or, if the event doesn't have a target $detach = $events->attach(function(Operation\BeforeProcessEvent $event) { // … }); $detach(); // You can detach if you no longer want to listen.
The following example demonstrates how an invokable object may be attached to that same event type.
<?php namespace ICanBoogie class ValidateOperation { private $rules; public function __construct(array $rules) { $this->rules = $rules; } public function __invoke(Operation\BeforeProcessEvent $event, Operation $target) { // … } } // … /* @var $events EventCollection */ /* @var $rules array<string, mixed> */ $events->attach(new ValidateOperation($rules));
Attaching an event hook to a specific target
Using the attach_to()
method, an event hook can be attached to a specific target, and is only
invoked for that target.
<?php namespace ICanBoogie; use ICanBoogie\Routing\Controller; // … /* @var $events EventCollection */ $detach = $events->attach_to($controller, function(Controller\ActionEvent $event, Controller $target) { echo "invoked!"; }); $controller_clone = clone $controller; emit(new Controller\ActionEvent($controller_clone, …)); // nothing happens, it's a clone emit(new Controller\ActionEvent($controller, …)); // echo "invoked!" // … $detach(); // You can detach if you no longer want to listen.
Attaching a one-time event hook
The once()
method attaches an event hook that is automatically detached after it's been used.
<?php namespace ICanBoogie; /* @var $events EventCollection */ $n = 0; $events->once(MyEvent $event, function() use(&$n) { $n++; }); emit(new MyEvent()); emit(new MyEvent()); emit(new MyEvent()); echo $n; // 1
Attaching event hooks using the events
config
When the package is bound to ICanBoogie by icanboogie/bind-event, event hooks may be
attached from the events
config. Have a look at the icanboogie/bind-event package for
further details.
Attaching event hooks to the finish chain
The finish chain is executed after the event chain was traversed without being stopped.
The following example demonstrates how an event hook may be attached to the finish chain of
the count
event to obtain the string "0123". If the third event hook was defined like the
others we would obtain "0312".
<?php namespace ICanBoogie; class CountEvent extends Event { public function __construct( public string $count = "0" ) { parent::__construct(); } } /* @var $events EventCollection */ $events->attach(function(CountEvent $event): void { $event->count .= "2"; }); $events->attach(function(CountEvent $event): void { $event->count .= "1"; }); $events->attach(function(CountEvent $event): void { $event->chain(function(CountEvent $event) { $event->count .= "3"; }); }); $event = emit(new CountEvent(0)); echo $event->count; // 0123
Breaking an event hook chain
The processing of an event hook chain can be broken by an event hook using the stop()
method:
<?php use ICanBoogie\Operation; function on_event(Operation\ProcessEvent $event, Operation $operation): void { $event->rc = true; $event->stop(); }
Profiling events
The EventProfiler class is used to collect timing information about unused events and event hook calls. All time information is measured in floating microtime.
<?php use ICanBoogie\EventProfiler; foreach (EventProfiler::$unused as list($time, $type)) { // … } foreach (EventProfiler::$calls as list($time, $type, $hook, $started_at)) { // … }
Helpers
get_events()
: Returns the current event collection. A new one is created if none exist.emit()
: Emit the specified event.
Continuous Integration
The project is continuously tested by GitHub actions.
Code of Conduct
This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.
Contributing
See CONTRIBUTING for details.