Skip to content
Shesh Ghimire edited this page Dec 31, 2024 · 36 revisions

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.

Event Manager

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 false will never listen for that EventType.
  • Event Dispatcher for each EventType can only be set once.

Before Build Event

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.

Building Event

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;
    }
}

Using Builder Pattern

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: RateLimiterConfig below
  • The Parameter Name for Rate Limiters must end with RateLimiter.
    • See: $web1MinuteRateLimiter and $apiRateLimiter accepted by WebMiddleware and ApiMiddleware below
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.

Using PHP Attribute

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.

Clone this wiki locally