-
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\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
falsewill never listen for that EventType. - Event Dispatcher for each EventType can only be set once.
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.
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) can be used instead:
$app->set(Accessible::class, 'arrayValues');
$app->when(EventType::BeforeBuild)
->for(Accessible::class)
->listenTo(weakMapProviderEventListener(...));
$app->get(Accessible::class)->getStack() instanceof WeakMap; // true.==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 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.
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 {
$hasLimitReached = // ...Compute rate limit based on fixed window policy with "$noOfTokens".
return $hasLimitReached;
}
}
class SlidingRateLimiter implements LimiterInterface {
public function consume(int $noOfTokens = 1): bool {
$hasLimitReached = // ...Compute rate limit based on sliding window policy with "$noOfTokens".
return $hasLimitReached;
}
}This is useful where users have flexibility to create multiple configurable instances of the same class.
Lets say project's Rate Limiter is configured in such a way that:
- It has a provider class that provides Rate Limiters based on $id and $policy.
- See:
RateLimiterProviderbelow
- See:
- The injected Rate Limiter's Parameter Name must end with RateLimiter.
- See:
$web1MinuteRateLimiterand$apiRateLimiteraccepted byWebMiddlewareandApiMiddlewarebelow
- 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 = $app->get(RateLimiterProvider::class);
// 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" with "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
) {}
}
// Event Listener will get registered automatically from PHP Attributes.
$app->get(WebMiddleware::class)->webRateLimiter instanceof FixedRateLimiter; // true.
$app->get(ApiMiddleware::class)->apiRateLimiter instanceof SlidingRateLimiter; // true.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 returning it back to the user.
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.
- Updaters: When the concrete's resolved instance properties needs to be updated (either directly or 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 as provided for the same concrete, decorators are resolved first, and only then updaters are invoked (for the last resolved decorator instance).
Let's say only digital goods are sold by a company. Project is setup like so:
use ArrayObject;
use ArrayAccess;
function customerDetailsEventListener(AfterBuildEvent $event): void {
$event
->decorateWith(MerchCustomerDetails::class)
->update(
function(Customer $customer): void {
$personalInfo = $customer->getPersonalInfo();
$personalInfo['first_name'] = 'John';
$personalInfo['last_name'] = 'Doe';
$personalInfo['age'] = '41';
$customer->setPersonalInfo($personalInfo);
}
);
}
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 billingInfoToArray(): 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 {
// Personal Info is added using Event Listener.
return $this->personalInfo;
}
public function getAddress(): ArrayAccess {
return $this->address;
}
public function personalInfoToArray(): array {
return array(
'firstName' => $this->personalInfo['first_name'],
'lastName' => $this->personalInfo['last_name'],
'age' => (int) $this->personalInfo['age'],
);
}
public function billingInfoToArray(): array {
return array(
'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 billing address. Instead of updating the CustomerDetails class, a new MerchCustomerDetails Decorator class is introduced:
class MerchCustomerDetails implements Customer {
public function __construct(
private Customer $customer,
private ?ArrayAccess $billingAddress = 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 billingInfoToArray(): array {
$address = $this->customer->billingInfoToArray();
if (null === $this->billingAddress) {
return $address;
}
// Changes behavior on how billing address is generated.
return array(
'state' => $this->billingAddress['state'] ?? $address['state'],
'country' => $this->billingAddress['country'] ?? $address['country'],
'zipCode' => (int) ($this->billingAddress['zip_code'] ?? $address['zip_code']),
);
}
}// 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 dispatched.
$customer = $app->get(Customer::class);
$customer instanceof CustomerDetails; // true.
empty($customer->getPersonalInfo()); // true.
// For the decorator, provide "ArrayObject" instance with "zip_code" initialized.
$app->when(MerchCustomerDetails::class)
->needs(ArrayAccess::class)
->give(static fn(): ArrayAccess => new ArrayObject(['zip_code' => '44800']));
// 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 dispatched.
$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' => 44800, // from MerchCustomerDetails::$billingAddress.
] === $customer->billingInfoToArray(); // true.This is helpful when:
- decoration is internal, or
- multiple changes are required in configuration to decorate the existing class.
instead of updating configurations, a PHP Attribute can be used to provide Event Listener directly to the class that needs to be decorated.
Continuing from example above.
#[DecorateWith('customerDetailsEventListener')]
class CustomerDetails implements Customer {
// ...implements as shown in example above.
}