-
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 as singleton is already instantiated.
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\Data\SharedBinding;
use TheWebSolver\Codegarage\Container\Event\BuildingEvent;
function fixedRateLimiterEventListener(BuildingEvent $e): void {
$e->setBinding(new Binding(FixedRateLimiter::class));
}
function slidingRateLimiterEventListener(BuildingEvent $e): void {
// Make Sliding Rate Limiter a singleton instance.
$e->setBinding(new SharedBinding(SlidingRateLimiter::class));
}
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 Config class that resolves Rate Limiters based on $id and $policy.
- See:
RateLimiterConfigbelow
- See:
- The Parameter Name for Rate Limiters must end with RateLimiter.
- See:
$web1MinuteRateLimiterand$apiRateLimiteraccepted byWebMiddlewareandApiMiddlewarebelow
- See:
use TheWebSolver\Codegarage\Container\Container;
use TheWebSolver\Codegarage\Container\Event\EventType;
final class RateLimiterConfig {
public function __construct(private Container $app) {}
public function register(string $id, string $policy): void {
$entry = 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);
}
}
// User registers multiple rate limiters:
$config = $app->get(RateLimiterConfig::class);
$config->register(id: 'web15Seconds', policy: 'fixed');
$config->register(id: 'web1Minute', policy: 'fixed');
$config->register(id: 'api', policy: 'sliding');
// User creates middlewares and add dependencies according to "$id" and "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.
$app->get(ApiMiddleware::class)->apiRateLimiter instanceof SlidingRateLimiter; // true.
$app->get(ServiceThatNeedsApiRateLimiter::class)->apiRateLimiter instanceof SlidingRateLimiter; // true.This is useful where a predefined config 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.