league/container
PSR-11–compliant dependency injection container from The PHP League. Register services, factories and shared instances, then resolve dependencies with autowiring support. Modern PHP (8.3+) with full docs, tests, and MIT license.
The League Container event system provides a way to hook into the container's lifecycle and modify services during resolution. Events are dispatched at key points during the container's operation, allowing you to extend functionality without modifying core container code.
The event system replaces inflectors, providing a more flexible and powerful alternative. See afterResolve() below for the simplest migration path.
The event system is designed to be:
For the most common use case, applying cross-cutting behaviour to resolved services by type, use afterResolve():
<?php
use League\Container\Container;
$container = new Container();
$container->afterResolve(LoggerAwareInterface::class, function (object $service) use ($logger) {
$service->setLogger($logger);
});
$container->afterResolve(CacheAwareInterface::class, function (object $service) use ($cache) {
$service->setCache($cache);
});
The callback receives the resolved object directly. This is a drop-in replacement for the deprecated inflector() method.
For more control, use the full event API with listen().
The container dispatches four types of events during its lifecycle:
Fired when a service definition is added to the container via add() or addShared().
<?php
use League\Container\Event\OnDefineEvent;
$container->listen(OnDefineEvent::class, function (OnDefineEvent $event) {
echo "Service '{$event->getId()}' was defined\n";
$definition = $event->getDefinition();
});
Fired just before resolution begins. Can short-circuit resolution by providing an early result via setResolved().
<?php
use League\Container\Event\BeforeResolveEvent;
$container->listen(BeforeResolveEvent::class, function (BeforeResolveEvent $event) {
if ($event->getId() === 'forbidden.service') {
$event->stopPropagation();
throw new AccessDeniedException();
}
});
Fired after a definition is found but before the object is instantiated. Can provide an alternative resolution.
<?php
use League\Container\Event\DefinitionResolvedEvent;
$container->listen(DefinitionResolvedEvent::class, function (DefinitionResolvedEvent $event) {
$definition = $event->getDefinition();
echo "Definition found for '{$event->getId()}'\n";
});
Fired after a service has been fully resolved. This is the most commonly used event for service modification.
<?php
use League\Container\Event\ServiceResolvedEvent;
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$service = $event->getResolved();
$service->setResolvedAt(new DateTime());
})->forType(TimestampableInterface::class);
afterResolve() is a convenience method that wraps the event system for the most common use case: applying modifications to resolved services by type. It is the recommended replacement for the deprecated inflector() method.
<?php
$container->afterResolve(LoggerAwareInterface::class, function (object $service) use ($logger) {
$service->setLogger($logger);
});
<?php
// Before
$container->inflector(LoggerAwareInterface::class, fn($obj) => $obj->setLogger($logger));
// After
$container->afterResolve(LoggerAwareInterface::class, fn($obj) => $obj->setLogger($logger));
afterResolve() returns an EventFilter, so you can add further constraints:
<?php
$container->afterResolve(LoggerAwareInterface::class, function (object $service) use ($logger) {
$service->setLogger($logger);
})->forTag('needs-logging');
The callback receives the resolved object directly and can mutate it. To replace the resolved object entirely (e.g., wrapping it in a decorator), use the full event API:
<?php
use League\Container\Event\ServiceResolvedEvent;
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$event->setResolved(new CachedRepository($event->getResolved()));
})->forType(RepositoryInterface::class);
Events can be filtered to only execute under specific conditions.
Listen only for specific resolved object types (only works with ServiceResolvedEvent):
<?php
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$event->getResolved()->setCreatedAt(new DateTime());
})->forType(UserInterface::class, AdminInterface::class);
Listen for services with specific tags:
<?php
$container->addShared('user.service', UserService::class)
->addTag('logging');
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) use ($container) {
$event->getResolved()->setLogger($container->get(LoggerInterface::class));
})->forTag('logging');
Listen for specific service IDs:
<?php
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$event->getResolved()->setRole('admin');
})->forId('admin.user', 'super.admin');
Use closures for complex filtering. Multiple where() calls compose with AND semantics (all must pass):
<?php
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$event->getResolved()->setSpecial(true);
})->forType(UserInterface::class)
->where(fn ($event) => str_starts_with($event->getId(), 'admin.'));
All filter types can be combined. They all must match for the listener to fire:
<?php
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$event->getResolved()->setAdminConfig(true);
})->forType(UserInterface::class)
->forTag('admin')
->where(fn ($event) => str_starts_with($event->getId(), 'admin.'));
When an event is dispatched, listeners are executed in the following order:
addListener() execute first, in registration orderlisten()->then() execute second, in registration orderIf a direct listener calls stopPropagation(), no filtered listeners will execute for that event.
removeListener() only removes listeners registered via addListener(). Listeners registered via listen()->then() (filtered listeners) cannot be individually removed. Use removeListeners() to clear all listeners and filters for a given event type.
Events are only dispatched when listeners are registered for that specific event type. If no listeners exist for BeforeResolveEvent, no BeforeResolveEvent objects are created during resolution. This means the event system has near-zero overhead when not in use.
You can check whether listeners exist for a given event type:
<?php
$dispatcher = $container->getEventDispatcher();
$dispatcher->hasListenersFor(ServiceResolvedEvent::class); // true or false
You can work directly with the event dispatcher for advanced use cases:
<?php
$dispatcher = $container->getEventDispatcher();
$dispatcher->addListener(ServiceResolvedEvent::class, $listener);
Events implement StoppableEventInterface and can halt propagation:
<?php
use League\Container\Event\BeforeResolveEvent;
$container->listen(BeforeResolveEvent::class, function (BeforeResolveEvent $event) {
if (!isAuthorised($event->getId())) {
$event->stopPropagation();
throw new UnauthorisedException();
}
});
Replace resolved objects with decorators or proxies:
<?php
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$original = $event->getResolved();
$cached = new CachedUserRepository($original);
$event->setResolved($cached);
})->forType(UserRepositoryInterface::class);
Events are dispatched for services resolved through delegate containers as well. This is useful when using ReflectionContainer for auto-wiring:
<?php
use League\Container\Container;
use League\Container\ReflectionContainer;
$container = new Container();
$container->delegate(new ReflectionContainer());
$container->addShared(DatabaseInterface::class, PDODatabase::class);
$container->addShared(LoggerInterface::class, MonologLogger::class);
$container->afterResolve(LoggerAwareInterface::class, function (object $service) use ($container) {
$service->setLogger($container->get(LoggerInterface::class));
});
$container->afterResolve(DatabaseAwareInterface::class, function (object $service) use ($container) {
$service->setDatabase($container->get(DatabaseInterface::class));
});
$userService = $container->get(UserService::class);
Use events to create different behaviours for testing:
<?php
$container = new Container();
$container->delegate(new ReflectionContainer());
if ($environment === 'testing') {
$container->addShared(EmailService::class, MockEmailService::class);
$container->addShared(PaymentGateway::class, FakePaymentGateway::class);
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
if (!$event->getDefinition()) {
TestLogger::log("Auto-wired: {$event->getId()}");
}
});
}
$userController = $container->get(UserController::class);
$emailService = $container->get(EmailService::class);
afterResolve() or forType() for type-based filtering, it uses instanceof checks which are faster than custom closuresforType() only with ServiceResolvedEvent, it has no effect on other event types<?php
// Faster: uses instanceof check
$container->listen(ServiceResolvedEvent::class, $listener)
->forType(UserInterface::class);
// Slower: executes custom function for every event
$container->listen(ServiceResolvedEvent::class, $listener)
->where(fn ($e) => $e->getResolved() instanceof UserInterface);
How can I help you explore Laravel packages today?