-
Notifications
You must be signed in to change notification settings - Fork 0
Event
Event provides a pub/sub interface to listen for events when a concrete passes through various build stages.
If you haven't already, refer to this page to learn how to bootstrap and use container.
NOTE: Although Event helps to perform additional tasks during build process, in most cases a simple registration might be sufficient. It is recommended to use this feature only when some not-so-simple tasks needs to performed when resolving a concrete.
By default, an Event Manager manages Event Dispatcher for all build stage events defined in EventType
enum. To disable event dispatcher for any event type:
use TheWebSolver\Codegarage\Container\Container;
use TheWebSolver\Codegarage\Container\Event\EventType;
use TheWebSolver\Codegarage\Container\Event\Manager\EventManager;
$eventManager = new EventManager();
// Prevent listening for event listeners during build stage.
$eventManager->setDispatcher(false, EventType::Building);
$app = new Container(eventManager: $eventManager);
// Set the above instantiated Container as a singleton instance.
// However, if `Container::boot()` is already invoked before,
// "$app" won't be set coz singleton is already initialized.
Container::use($app);
Container::boot() === $app; // true. Container with "EventType::Building" disabled.
- Event Dispatcher set as
false
will never listen for that EventType. - Event Dispatcher for each EventType can only be set once.
Event Listener can be a function (named or lambda), or a class method. Event Listener accepts an event instance based on EventType being listened for.
An entry can have as many event listeners as needed by the project. But most of the time, there might only be a single event listener registered.
Container provides fluent interface using Builder Pattern to register an Event Listener for an entry of a certain EventType.
This registration process is the RECOMMENDED way to register an Event Listener and will be used throughout Event documentation.
$app->when($eventType) // One of "EventType" case.
->for($entry) // Entry varies based on "EventType" being used.
->listenTo(
listener: anEventListener(...), // listener accepts an $event object based on EventType.
priority: 10 // Default is 10. Lower the number, earlier it will be listened.
);
// Adding another listener for same "$entry" which'll be listened after "anEventListener".
$app->when($eventType)
->for($entry)
->listenTo(delayedEventListener(...), priority: 20);
Container also provides a getter method to get an Event Listener Registry of a certain EventType. In most cases, simple registration should suffice.
$app->getListenerRegistry($eventType)?->addListener(
anEventListener(...),
$entry,
$priority
);
It may also be used in following cases:
-
To be informed about the highest and lowest priorities that was set before registering the new listener.
['high' => $highest, 'low' => $lowest] = $app->getListenerRegistry($eventType)?->getPriorities($entry);
-
To check if event listeners are already registered.
$hasListenerForEntry = $app->getListenerRegistry($eventType)?->hasListeners($entry);
-
To get/reset event listeners that are already registered.
$entryListeners = $app->getListenerRegistry($eventType)?->getListeners($entry); $app->getListenerRegistry($eventType)?->reset($entry);
Event dispatched by Event Dispatcher implements:
- Taggable: provides an entry name for whom listeners are listened by the dispatcher.
- Stoppable: represents event has been completed and prevents further Listeners from being called.
Below example uses Before Build Event. See BeforeBuildEvent section to learn more.
use ArrayObject;
use TheWebSolver\Codegarage\Container\Event\EventType;
use TheWebSolver\Codegarage\Container\Event\BeforeBuildEvent;
function arrayProviderEventListener(BeforeBuildEvent $event) {
$event->setParam('array', [1,2,3]);
$event->stopPropagation();
}
function appendValuesEventListener(BeforeBuildEvent $event) {
$event->setParam('array', array(...$event->getParams()['array'], 4,5));
}
$app->when(EventType::BeforeBuild)
->for(ArrayObject::class)
->listenTo(arrayProviderEventListener(...));
$app->when(EventType::BeforeBuild)
->for(ArrayObject::class)
->listenTo(appendValuesEventListener(...));
// The "appendValuesEventListener" will never be listened.
// (the listener priority number must be same or higher)
$app->get(ArrayObject::class)->getArrayCopy() === [1,2,3]; // true.
This is the first stage in the build process of any concrete. Here, an Event Dispatcher will dispatch a BeforeBuildEvent. All listeners registered will accept this event to SET/GET/UPDATE the concrete's dependency based on Parameter Name.
use WeakMap;
use ArrayAccess;
use TheWebSolver\Codegarage\Container\Event\EventType;
use TheWebSolver\Codegarage\Container\Event\BeforeBuildEvent;
function weakMapProviderEventListener(BeforeBuildEvent $e): void {
// It is actually possible but NOT RECOMMENDED to perform entry checks in event listeners.
// But, be aware this event listener must not be registered for a concrete that injects
// same Parameter Name but the expected value is a string instead of a WeakMap object.
// If needed, create another event listener to follow Single Responsibility Principle.
$shouldListenForEntry = match ($e->getEntry()) {
Access::class, 'arrayValues', Accessible::class => true,
default => false,
};
// It is scoped for an Access class, its alias or an interface it is registered to.
if ($shouldListenForEntry) {
$e->setParam(name: 'stack', value: new WeakMap());
}
}
interface Accessible {
public function getStack(): ArrayAccess;
}
class Access implements Accessible {
public function __construct(private ArrayAccess $stack) {}
public function getStack(): ArrayAccess {
return $this->stack;
}
}
$app->when(EventType::BeforeBuild)
->for(Access::class)
->listenTo(weakMapProviderEventListener(...));
$app->get(Access::class)->getStack() instanceof WeakMap; // true.
// Or, when classname is aliased, alias can be used instead:
$app->setAlias(Access::class, 'arrayValues');
$app->when(EventType::BeforeBuild)
->for('arrayValues')
->listenTo(weakMapProviderEventListener(...));
$app->get('arrayValues')->getStack() instanceof WeakMap; // true.
// Or, when classname or its alias is registered to an interface,
// interface (highly recommended) must be used:
$app->set(Accessible::class, 'arrayValues');
$app->when(EventType::BeforeBuild)
->for(Accessible::class)
->listenTo(weakMapProviderEventListener(...));
$app->get(Accessible::class)->getStack() instanceof WeakMap; // true.
Before Build Event's entry name varies based on how a concrete is registered to the Container.
use WeakMap;
use ArrayAccess;
use TheWebSolver\Codegarage\Container\Event\EventType;
use TheWebSolver\Codegarage\Container\Event\BeforeBuildEvent;
// LISTENER: 1: Without registering concrete to the container.
$app->when(EventType::BeforeBuild)
->for(WeakMap::class)
->listenTo(function(BeforeBuildEvent $e) {});
$app->getListenerRegistry(EventType::BeforeBuild)->hasListeners(WeakMap::class); // true.
// Registering concrete as an alias.
$app->setAlias(WeakMap::class, 'arrayValues');
// LISTENER: 2
$app->when(EventType::BeforeBuild)
->for('arrayValues')
// Or,
// ->for(WeakMap::class)
->listenTo(function(BeforeBuildEvent $e) {});
$app->getListenerRegistry(EventType::BeforeBuild)->hasListeners(WeakMap::class); // true.
$app->getListenerRegistry(EventType::BeforeBuild)->hasListeners('arrayValues'); // false.
// Registering concrete to an interface.
$app->set(ArrayAccess::class, WeakMap::class);
// LISTENER: 3
$app->when(EventType::BeforeBuild)
->for(ArrayAccess::class)
->listenTo(function(BeforeBuildEvent $e) {});
$app->getListenerRegistry(EventType::BeforeBuild)->hasListeners(ArrayAccess::class); // true.
// If LISTENER 1 or 2 is also registered, returns "true". Else "false".
$app->getListenerRegistry(EventType::BeforeBuild)->hasListeners(WeakMap::class);
==EventType::Building
stage is disabled as documented in Event Manager section, then LogicalError is thrown when using Builder Pattern.==
This is the second stage in the build process of any concrete. Here, an Event Dispatcher will dispatch a BuildingEvent. All listeners registered will accept this event to resolve dependency based on dependency Parameter TypeHint and Parameter Name.
This sounds a lot like Before Build Event but the intention is very different. Here, instead of the concrete classname, its injected dependency parameter's name and type-hint is used as an entry. Meaning, wherever this exact parameter name and type-hint is used, it gets resolved irrespective of the concrete classname it is injected on.
The main use case of Building Event is to resolve some pre-defined dependencies based on the project's configuration.
Below example promotes Liskov Substitution Principle to allow swappable configuration without project's other code alteration.
use TheWebSolver\Codegarage\Container\Data\Binding;
use TheWebSolver\Codegarage\Container\Event\BuildingEvent;
function fixedRateLimiterEventListener(BuildingEvent $e): void {
$e->setBinding(new Binding(FixedRateLimiter::class));
}
function slidingRateLimiterEventListener(BuildingEvent $e): void {
// Register Sliding Rate Limiter as shared (singleton).
$e->setBinding(new Binding(SlidingRateLimiter::class, isShared: true));
}
interface LimiterInterface {
public function consume(int $noOfTokens = 1): bool
}
class FixedRateLimiter implements LimiterInterface {
public function consume(int $noOfTokens = 1): bool {
// ...Compute rate limit based on fixed window policy with "$noOfTokens".
$hasLimitReached = true;
return $hasLimitReached;
}
}
class SlidingRateLimiter implements LimiterInterface {
public function consume(int $noOfTokens = 1): bool {
// ...Compute rate limit based on sliding window policy with "$noOfTokens".
$hasLimitReached = false;
return $hasLimitReached;
}
}
This is useful where user has the flexibility to create multiple configurable instances of the same class.
Lets say, standard set below must be followed to use Rate Limiter:
- User must use Provider class that provides Rate Limiter based on $id and $policy.
- See:
RateLimiterProvider
below
- See:
- The concrete that needs Rate Limiter must inject in its constructor by typ-hinting
LimiterInterface
and Parameter Name using the $id + RateLimiter suffix.- See:
$web1MinuteRateLimiter
and$apiRateLimiter
accepted byWebMiddleware
andApiMiddleware
below
- See:
use TheWebSolver\Codegarage\Container\Container;
use TheWebSolver\Codegarage\Container\Event\EventType;
final class RateLimiterProvider {
public function __construct(private Container $app) {}
public function provideFor(string $id, string $policy): void {
$entry = lcfirst(trim($id)); // ...Perform transformation as needed.
$paramName = "{$entry}RateLimiter";
$eventListener = 'fixed' === $policy
? fixedRateLimiterEventListener(...)
: slidingRateLimiterEventListener(...)
$this->app->when(EventType::Building)
->for(LimiterInterface::class, $paramName)
->listenTo($eventListener);
}
}
$provider = new RateLimiterProvider($app);
// User registers multiple rate limiters:
$provider->provideFor(id: 'web15Seconds', policy: 'fixed');
$provider->provideFor(id: 'Web1Minute', policy: 'fixed');
$provider->provideFor(id: 'api', policy: 'sliding');
// User creates middlewares and inject dependencies following set standard.
// Parameter name must be a transformed "$id" + "RateLimiter" suffix.
class WebMiddleware {
public function __construct(public readonly LimiterInterface $web1MinuteRateLimiter) {}
}
class ApiMiddleware {
public function __construct(public readonly LimiterInterface $apiRateLimiter) {}
}
class ServiceThatNeedsApiRateLimiter {
public function __construct(public readonly LimiterInterface $apiRateLimiter) {}
}
$app->get(WebMiddleware::class)->web1MinuteRateLimiter instanceof FixedRateLimiter; // true.
// Concretes that inject parameter with same type-hint and name. Because SlidingRateLimiter is
// set as shared, same Rate Limiter instance is injected instead of two separate instances.
$middlewareRateLimiter = $app->get(ApiMiddleware::class)->apiRateLimiter;
$serviceRateLimiter = $app->get(ServiceThatNeedsApiRateLimiter::class)->apiRateLimiter;
$middlewareRateLimiter instanceof SlidingRateLimiter; // true.
$serviceRateLimiter instanceof SlidingRateLimiter; // true.
$middlewareRateLimiter === $serviceRateLimiter; // true.
This is useful where a predefined/fixed configuration must be used.
use TheWebSolver\Codegarage\Container\Attribute\ListenTo;
class WebMiddleware {
public function __construct(
#[ListenTo('fixedRateLimiterEventListener')]
public readonly LimiterInterface $webRateLimiter
) {}
}
class ApiMiddleware {
public function __construct(
#[ListenTo('slidingRateLimiterEventListener')]
public readonly LimiterInterface $apiRateLimiter
) {}
}
$app->get(WebMiddleware::class)->webRateLimiter instanceof FixedRateLimiter; // true.
$app->get(ApiMiddleware::class)->apiRateLimiter instanceof SlidingRateLimiter; // true.
Building Event's entry name is the combination of parameter type-hint and parameter name.
Continuing from Builder Pattern example above:
$app->getListenerRegistry(EventType::AfterBuild)
->hasListeners(LimiterInterface::class . ':apiRateLimiter'); // true.
$app->getListenerRegistry(EventType::AfterBuild)
->hasListeners(LimiterInterface::class); // false.
$app->getListenerRegistry(EventType::AfterBuild)
->hasListeners('apiRateLimiter'); // false.
This is the final stage in the build process of any concrete. Here, an Event Dispatcher will dispatch an AfterBuildEvent. All listeners registered will accept this event to UPDATE/DECORATE the concrete's instance.
This is useful when some initialization is needed immediately after concrete is instantiated but before getting it back from the container.
This stage provides two distinct features:
- Decorators: When the concrete's resolved instance needs some behavioral changes inside its methods. Decorators must inject the resolved instance as first parameter (type-hint to an interface and not the concrete) in it's constructor. This promotes Open/Closed Principle where already existing concrete class should not be modified when additional functionality is to be introduced.
- Updaters: When the concrete's resolved instance needs to be updated (using setter/getter method). Updaters must accept the resolved instance as first parameter (type-hint to an interface and not the concrete) and optionally, Container as second parameter.
When both decorators and updaters are provided for the same concrete, decorators are resolved first, and only then updaters are invoked (for the last resolved decorator instance if decorators are provided).
See After Build Event Feature Test using AfterBuildEvent Wiki Codes shown below.
Let's say only digital goods are sold by a company. Project is setup like so:
use ArrayAccess;
interface Customer {
public function setPersonalInfo(ArrayAccess $details): void;
public function getPersonalInfo(): ArrayAccess;
public function getAddress(): ArrayAccess;
/** @return array{firstName:string,lastName:string,age:int} */
public function personalInfoToArray(): array;
/** @return array{state:string,country:string,zipCode:int} */
public function addressToArray(): array;
}
class CustomerDetails implements Customer {
public function __construct(
private ArrayAccess $personalInfo,
private ArrayAccess $address,
) {
$address['state'] = 'Bagmati';
$address['country'] = 'Nepal';
$address['zip_code'] = '44811';
}
public function setPersonalInfo(ArrayAccess $details): void {
$this->personalInfo = $details;
}
public function getPersonalInfo(): ArrayAccess {
return $this->personalInfo;
}
public function getAddress(): ArrayAccess {
return $this->address;
}
public function personalInfoToArray(): array {
return [
'firstName' => $this->personalInfo['first_name'],
'lastName' => $this->personalInfo['last_name'],
'age' => (int) $this->personalInfo['age'],
];
}
public function addressToArray(): array {
return [
'state' => $this->address['state'],
'country' => $this->address['country'],
'zipCode' => (int) $this->address['zip_code'],
];
}
}
Later, company started providing some free Merch to their loyal customers and wanted to give customers flexibility to set their shipping address. Instead of updating the CustomerDetails
class, a new MerchCustomerDetails
Decorator class is introduced:
use ArrayAccess;
use ArrayObject;
use TheWebSolver\Codegarage\Container\Event\AfterBuildEvent;
function customerDetailsEventListener(AfterBuildEvent $event): void {
// When resolving decorator, provide "ArrayObject" instance with "zip_code" initialized.
$event->app()->when(MerchCustomerDetails::class)
->needs(ArrayAccess::class)
->give(static fn(): ArrayAccess => new ArrayObject(['zip_code' => '44600']));
$event
->decorateWith(MerchCustomerDetails::class)
->update(
static function(Customer $customer): void {
$personalInfo = $customer->getPersonalInfo();
$personalInfo['first_name'] = 'John';
$personalInfo['last_name'] = 'Doe';
$personalInfo['age'] = '41';
$customer->setPersonalInfo($personalInfo);
}
);
}
class MerchCustomerDetails implements Customer {
public function __construct(
private Customer $customer,
private ?ArrayAccess $shippingAddress = null,
) {}
public function setPersonalInfo(ArrayAccess $details): void {
$this->customer->setPersonalInfo($details);
}
public function getPersonalInfo(): ArrayAccess {
return $this->customer->getPersonalInfo();
}
public function getAddress(): ArrayAccess {
return $this->customer->getAddress();
}
public function personalInfoToArray(): array {
return $this->customer->personalInfoToArray();
}
public function addressToArray(): array {
$address = $this->customer->addressToArray();
if (null === $this->shippingAddress) {
return $address;
}
// Changes behavior on how shipping address is generated.
return [
'state' => $this->shippingAddress['state'] ?? $address['state'],
'country' => $this->shippingAddress['country'] ?? $address['country'],
'zipCode' => (int) ($this->shippingAddress['zip_code'] ?? $address['zip_code']),
];
}
}
use ArrayAccess;
use ArrayObject;
use TheWebSolver\Codegarage\Container\Event\EventType;
// Register to container.
$app->set(Customer::class, CustomerDetails::class);
// Provide the "ArrayObject" class for "ArrayAccess" interface.
$app->when(CustomerDetails::class)
->needs(ArrayAccess::class)
->give(ArrayObject::class);
// Before event listeners are registered.
$customer = $app->get(Customer::class);
$customer instanceof CustomerDetails; // true.
empty($customer->getPersonalInfo()); // true.
// Decorate "CustomerDetails" class with "MerchCustomerDetails", and update
// "CustomerDetails::$personalInfo" property with default values.
$app->when(EventType::AfterBuild)
->for(Customer::class)
->listenTo(customerDetailsEventListener(...));
// After event listeners are registered.
$customer = $app->get(Customer::class);
$customer instanceof MerchCustomerDetails; // true.
$customer->personalInfoToArray()['firstName'] === 'John'; // true.
[
'state' => 'Bagmati', // from CustomerDetails::$address.
'country' => 'Nepal', // from CustomerDetails::$address.
'zipCode' => 44600, // from MerchCustomerDetails::$shippingAddress.
] === $customer->addressToArray(); // true.
This is useful when:
- decoration is internal, or
- overcome multiple configuration changes required to decorate an existing class.
Continuing from example above.
use TheWebSolver\Codegarage\Container\Attribute\DecorateWith;
#[DecorateWith('customerDetailsEventListener')]
class CustomerDetails implements Customer {
// ...implements as shown in example above.
}
After Build Event's entry name is the concrete classname that will be resolved by the Container.
Continuing from Builder Pattern example above:
$app->getListenerRegistry(EventType::AfterBuild)->hasListeners(CustomerDetails::class); // true.
$app->getListenerRegistry(EventType::AfterBuild)->hasListeners(MerchCustomerDetails::class); // false.
$app->getListenerRegistry(EventType::AfterBuild)->hasListeners(Customer::class); // false.