Uh oh!
There was an error while loading. Please reload this page.
- 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 EventTypeenum. To disable event dispatcher for any event type:
useTheWebSolver\Codegarage\Container\Container;
useTheWebSolver\Codegarage\Container\Event\EventType;
useTheWebSolver\Codegarage\Container\Event\Manager\EventManager;
$eventManager = newEventManager();
// Prevent listening for event listeners during build stage.$eventManager->setDispatcher(false, EventType::Building);
$app = newContainer(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
falsewill 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.
useArrayObject;
useTheWebSolver\Codegarage\Container\Event\EventType;
useTheWebSolver\Codegarage\Container\Event\BeforeBuildEvent;
functionarrayProviderEventListener(BeforeBuildEvent$event) {
$event->setParam('array', [1,2,3]);
$event->stopPropagation();
}
functionappendValuesEventListener(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.
useWeakMap;
useArrayAccess;
useTheWebSolver\Codegarage\Container\Event\EventType;
useTheWebSolver\Codegarage\Container\Event\BeforeBuildEvent;
functionweakMapProviderEventListener(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: newWeakMap());
}
}
interface Accessible {
publicfunctiongetStack(): ArrayAccess;
}
class Access implements Accessible {
publicfunction__construct(privateArrayAccess$stack) {}
publicfunctiongetStack(): 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.
useWeakMap;
useArrayAccess;
useTheWebSolver\Codegarage\Container\Event\EventType;
useTheWebSolver\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.
useTheWebSolver\Codegarage\Container\Data\Binding;
useTheWebSolver\Codegarage\Container\Event\BuildingEvent;
functionfixedRateLimiterEventListener(BuildingEvent$e): void {
$e->setBinding(newBinding(FixedRateLimiter::class));
}
functionslidingRateLimiterEventListener(BuildingEvent$e): void {
// Register Sliding Rate Limiter as shared (singleton).$e->setBinding(newBinding(SlidingRateLimiter::class, isShared: true));
}
interface LimiterInterface {
publicfunctionconsume(int$noOfTokens = 1): bool
}
class FixedRateLimiter implements LimiterInterface {
publicfunctionconsume(int$noOfTokens = 1): bool {
// ...Compute rate limit based on fixed window policy with "$noOfTokens".$hasLimitReached = true;
return$hasLimitReached;
}
}
class SlidingRateLimiter implements LimiterInterface {
publicfunctionconsume(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:
RateLimiterProviderbelow
- See:
- The concrete that needs Rate Limiter must inject in its constructor by typ-hinting
LimiterInterfaceand Parameter Name using the $id + RateLimiter suffix.- See:
$web1MinuteRateLimiterand$apiRateLimiteraccepted byWebMiddlewareandApiMiddlewarebelow
- See:
useTheWebSolver\Codegarage\Container\Container;
useTheWebSolver\Codegarage\Container\Event\EventType;
finalclass RateLimiterProvider {
publicfunction__construct(privateContainer$app) {}
publicfunctionprovideFor(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 = newRateLimiterProvider($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 {
publicfunction__construct(publicreadonlyLimiterInterface$web1MinuteRateLimiter) {}
}
class ApiMiddleware {
publicfunction__construct(publicreadonlyLimiterInterface$apiRateLimiter) {}
}
class ServiceThatNeedsApiRateLimiter {
publicfunction__construct(publicreadonlyLimiterInterface$apiRateLimiter) {}
}
$app->get(WebMiddleware::class)->web1MinuteRateLimiterinstanceof 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;
$middlewareRateLimiterinstanceof SlidingRateLimiter; // true.$serviceRateLimiterinstanceof SlidingRateLimiter; // true.$middlewareRateLimiter === $serviceRateLimiter; // true.This is useful where a predefined/fixed configuration must be used.
useTheWebSolver\Codegarage\Container\Attribute\ListenTo;
class WebMiddleware {
publicfunction__construct(
#[ListenTo('fixedRateLimiterEventListener')]
publicreadonlyLimiterInterface$webRateLimiter
) {}
}
class ApiMiddleware {
publicfunction__construct(
#[ListenTo('slidingRateLimiterEventListener')]
publicreadonlyLimiterInterface$apiRateLimiter
) {}
}
$app->get(WebMiddleware::class)->webRateLimiterinstanceof FixedRateLimiter; // true.$app->get(ApiMiddleware::class)->apiRateLimiterinstanceof 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:
useArrayAccess;
interface Customer {
publicfunctionsetPersonalInfo(ArrayAccess$details): void;
publicfunctiongetPersonalInfo(): ArrayAccess;
publicfunctiongetAddress(): ArrayAccess;
/** @return array{firstName:string,lastName:string,age:int} */publicfunctionpersonalInfoToArray(): array;
/** @return array{state:string,country:string,zipCode:int} */publicfunctionaddressToArray(): array;
}
class CustomerDetails implements Customer {
publicfunction__construct(
privateArrayAccess$personalInfo,
privateArrayAccess$address,
) {
$address['state'] = 'Bagmati';
$address['country'] = 'Nepal';
$address['zip_code'] = '44811';
}
publicfunctionsetPersonalInfo(ArrayAccess$details): void {
$this->personalInfo = $details;
}
publicfunctiongetPersonalInfo(): ArrayAccess {
return$this->personalInfo;
}
publicfunctiongetAddress(): ArrayAccess {
return$this->address;
}
publicfunctionpersonalInfoToArray(): array {
return [
'firstName' => $this->personalInfo['first_name'],
'lastName' => $this->personalInfo['last_name'],
'age' => (int) $this->personalInfo['age'],
];
}
publicfunctionaddressToArray(): 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:
useArrayAccess;
useArrayObject;
useTheWebSolver\Codegarage\Container\Event\AfterBuildEvent;
functioncustomerDetailsEventListener(AfterBuildEvent$event): void {
// When resolving decorator, provide "ArrayObject" instance with "zip_code" initialized.$event->app()->when(MerchCustomerDetails::class)
->needs(ArrayAccess::class)
->give(staticfn(): ArrayAccess => newArrayObject(['zip_code' => '44600']));
$event
->decorateWith(MerchCustomerDetails::class)
->update(
staticfunction(Customer$customer): void {
$personalInfo = $customer->getPersonalInfo();
$personalInfo['first_name'] = 'John';
$personalInfo['last_name'] = 'Doe';
$personalInfo['age'] = '41';
$customer->setPersonalInfo($personalInfo);
}
);
}
class MerchCustomerDetails implements Customer {
publicfunction__construct(
privateCustomer$customer,
private ?ArrayAccess$shippingAddress = null,
) {}
publicfunctionsetPersonalInfo(ArrayAccess$details): void {
$this->customer->setPersonalInfo($details);
}
publicfunctiongetPersonalInfo(): ArrayAccess {
return$this->customer->getPersonalInfo();
}
publicfunctiongetAddress(): ArrayAccess {
return$this->customer->getAddress();
}
publicfunctionpersonalInfoToArray(): array {
return$this->customer->personalInfoToArray();
}
publicfunctionaddressToArray(): 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']),
];
}
}useArrayAccess;
useArrayObject;
useTheWebSolver\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);
$customerinstanceof 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);
$customerinstanceof 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.
useTheWebSolver\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.