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.
composer require icanboogie/event
- 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.
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.
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();
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);
}
}
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.
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.
Events are fired with the emit()
function.
<?php
namespace ICanBoogie;
/* @var Event $event */
emit($event);
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));
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.
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
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.
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
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();
}
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))
{
// …
}
get_events()
: Returns the current event collection. A new one is created if none exist.emit()
: Emit the specified event.
The project is continuously tested by GitHub actions.
This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.
See CONTRIBUTING for details.