-
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 Events 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 dispatches 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.
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.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 same object.
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.
// User must use transformed "$id" as well as the "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.