Events
Introduction
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:
- Flexible - Multiple filtering options and event types
- Performant - Events are only dispatched when listeners are registered for that event type
- Extensible - Easy to add custom event logic
Quick Start
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().
Event Types
The container dispatches four types of events during its lifecycle:
OnDefineEvent
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();
});
BeforeResolveEvent
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();
}
});
DefinitionResolvedEvent
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";
});
ServiceResolvedEvent
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
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);
});
Migrating from inflector()
<?php
// Before
$container->inflector(LoggerAwareInterface::class, fn($obj) => $obj->setLogger($logger));
// After
$container->afterResolve(LoggerAwareInterface::class, fn($obj) => $obj->setLogger($logger));
Chaining filters
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');
Limitations
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);
Event Filtering
Events can be filtered to only execute under specific conditions.
Type-Based Filtering
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);
Tag-Based Filtering
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');
ID-Based Filtering
Listen for specific service IDs:
<?php
$container->listen(ServiceResolvedEvent::class, function (ServiceResolvedEvent $event) {
$event->getResolved()->setRole('admin');
})->forId('admin.user', 'super.admin');
Custom Filtering
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.'));
Combined Filtering
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.'));
Event Dispatcher
Execution Order
When an event is dispatched, listeners are executed in the following order:
- Direct listeners registered via
addListener()execute first, in registration order - Filtered listeners registered via
listen()->then()execute second, in registration order
If a direct listener calls stopPropagation(), no filtered listeners will execute for that event.
Listener Removal
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.
Performance
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
Direct Event Dispatcher Access
You can work directly with the event dispatcher for advanced use cases:
<?php
$dispatcher = $container->getEventDispatcher();
$dispatcher->addListener(ServiceResolvedEvent::class, $listener);
Stoppable Events
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();
}
});
Advanced Features
Object Transformation
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);
Working with Delegate Containers
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);
Testing Environment Setup
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);
Performance Tips
- Use
afterResolve()orforType()for type-based filtering, it usesinstanceofchecks which are faster than custom closures - Keep listeners focused with one responsibility per listener
- Use tags for grouping related services
- Use
forType()only withServiceResolvedEvent, 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);