Summary
Introduce first-class PHP attributes for registering event listeners in CakePHP 6.x, using the AttributeResolver system for discovery.
The feature is additive and keeps full compatibility with existing event registration patterns:
EventManager::on() callbacks
EventListenerInterface::implementedEvents() subscribers
- application/plugin
events() hooks
This RFC targets feature parity with the current event system and provides a modern, declarative API that reduces boilerplate.
Motivation
CakePHP already has a robust event system, but listener registration is currently imperative or array-based. Attribute-based registration offers:
- Better locality: event metadata lives next to handler methods.
- Lower boilerplate for common listener declarations.
- Better static discoverability with runtime cache support via AttributeResolver.
- A migration path familiar to developers from frameworks with attribute-driven listener registration.
Goals
- Provide attribute-based listener registration with parity to existing listener capabilities.
- Reuse
AttributeResolver for discovery, filtering, and caching.
- Keep behavior deterministic and aligned with
EventManager semantics.
- Keep the feature additive with no deprecations in 6.x.
Non-Goals
- Replacing
implementedEvents() in 6.x.
- Removing or deprecating imperative
on() registration.
- Introducing a new event dispatcher abstraction.
Current System (Baseline)
Existing behavior and extension points:
Proposed API
Listener Attribute
Add a new attribute class, for example Cake\Event\Attribute\EventListener, applicable to both methods and classes, and repeatable.
Proposed constructor shape:
public function __construct(
string $event,
int $priority = EventManager::$defaultPriority,
?string $method = null,
) {}
Semantics:
event: event name string (same event keys used by EventManager::on()), commonly a literal (for example 'foo') or a ::NAME constant.
priority: ordering parity with existing priority behavior.
method: optional explicit method name.
Repeatable attributes enable:
- one method to listen to multiple events.
- one class to declare multiple listeners.
Method resolution rules:
- If
method is provided, use it.
- For method-level attributes without
method, use the declaring method.
- For class-level attributes without
method:
- use
__invoke() if present.
- otherwise, infer method names (
on + normalized event identifier), for example foo => onFoo.
- If no resolvable method exists, treat the declaration as invalid and apply configured error handling.
Usage Examples
use Cake\Event\Attribute\EventListener;
use Cake\Event\EventInterface;
final class OrdersListener
{
#[EventListener('Order.afterPlace')]
public function sendReceipt(EventInterface $event): void
{
}
#[EventListener('Order.afterPlace', priority: 5)]
#[EventListener('Order.afterCancel', priority: 20)]
public function updateMetrics(EventInterface $event): void
{
}
}
use App\Event\CustomEvent;
use Cake\Event\Attribute\EventListener;
use Cake\Event\EventInterface;
#[EventListener(event: CustomEvent::NAME, method: 'onCustomEvent')]
#[EventListener(event: 'foo', priority: 42)]
#[EventListener(event: 'bar', method: 'onBarEvent')]
final class MyMultiListener
{
public function onCustomEvent(EventInterface $event): void
{
}
public function onFoo(): void
{
}
public function onBarEvent(): void
{
}
}
use Cake\Event\Attribute\EventListener;
use Cake\Event\EventInterface;
#[EventListener('Order.afterPlace')]
final class InvokableOrderListener
{
public function __invoke(EventInterface $event): void
{
}
}
Discovery and Registration Model
Discovery Source
Use an appropriate AttributeResolver collection/configuration to query targets decorated with EventListener.
Examples include a dedicated events config or a shared dafaullt config that contains both routing and event metadata.
Expected filtering chain:
withAttribute(EventListener::class)
withTargetType(AttributeTargetType::METHOD) and withTargetType(AttributeTargetType::CLASS_TYPE)
- optional namespace/class/plugin constraints by configuration
Registration Lifecycle
Listeners are attached through the existing application event bootstrap hooks:
This keeps behavior explicit and consistent with current docs guidance for registering app/plugin events.
Cache and Performance
Use a resolver configuration strategy with constrained scan paths. This may be a dedicated events config, or a shared config (for example core) used for multiple framework attribute domains such as routing and events:
Parity and Compatibility
Capability Parity
The attribute system must support parity with current event registration patterns:
- Event name mapping (
string keys).
- Listener priority.
- Multiple listeners per class/method via repeatable attribute.
- Class-level listeners with
__invoke() support.
- Dispatch behavior and stop propagation remain unchanged because dispatch continues through
EventManager.
Coexistence Rules
In 6.x, attributes coexist with implementedEvents() and manual on() registration.
- No deprecations are introduced in this RFC.
- Existing listener declarations remain fully supported.
- If the same callable is registered multiple times for the same event and priority via mixed mechanisms, registration should deduplicate by callable identity where feasible.
Ordering Guarantees
- Primary sort remains event priority, identical to current
EventManager behavior.
- For equal priority, preserve deterministic discovery/registration order (file and method line order) to avoid non-deterministic execution.
Error Handling
Invalid attribute declarations should fail early in development and tests, while avoiding hard failures in production if configured.
Proposed behavior:
- Debug/test mode: throw clear exceptions for invalid signatures or unresolved methods.
- Production mode: log warning and skip invalid listener declaration.
Summary
Introduce first-class PHP attributes for registering event listeners in CakePHP 6.x, using the AttributeResolver system for discovery.
The feature is additive and keeps full compatibility with existing event registration patterns:
EventManager::on()callbacksEventListenerInterface::implementedEvents()subscribersevents()hooksThis RFC targets feature parity with the current event system and provides a modern, declarative API that reduces boilerplate.
Motivation
CakePHP already has a robust event system, but listener registration is currently imperative or array-based. Attribute-based registration offers:
Goals
AttributeResolverfor discovery, filtering, and caching.EventManagersemantics.Non-Goals
implementedEvents()in 6.x.on()registration.Current System (Baseline)
Existing behavior and extension points:
events()thenpluginEvents()in src/Http/BaseApplication.php.events()andpluginEvents()in src/Console/CommandRunner.php.Proposed API
Listener Attribute
Add a new attribute class, for example
Cake\Event\Attribute\EventListener, applicable to both methods and classes, and repeatable.Proposed constructor shape:
Semantics:
event: event name string (same event keys used byEventManager::on()), commonly a literal (for example'foo') or a::NAMEconstant.priority: ordering parity with existing priority behavior.method: optional explicit method name.Repeatable attributes enable:
Method resolution rules:
methodis provided, use it.method, use the declaring method.method:__invoke()if present.on+ normalized event identifier), for examplefoo=>onFoo.Usage Examples
Discovery and Registration Model
Discovery Source
Use an appropriate
AttributeResolvercollection/configuration to query targets decorated withEventListener.Examples include a dedicated
eventsconfig or a shareddafaulltconfig that contains both routing and event metadata.Expected filtering chain:
withAttribute(EventListener::class)withTargetType(AttributeTargetType::METHOD)andwithTargetType(AttributeTargetType::CLASS_TYPE)Registration Lifecycle
Listeners are attached through the existing application event bootstrap hooks:
This keeps behavior explicit and consistent with current docs guidance for registering app/plugin events.
Cache and Performance
Use a resolver configuration strategy with constrained scan paths. This may be a dedicated
eventsconfig, or a shared config (for examplecore) used for multiple framework attribute domains such as routing and events:Parity and Compatibility
Capability Parity
The attribute system must support parity with current event registration patterns:
stringkeys).__invoke()support.EventManager.Coexistence Rules
In 6.x, attributes coexist with
implementedEvents()and manualon()registration.Ordering Guarantees
EventManagerbehavior.Error Handling
Invalid attribute declarations should fail early in development and tests, while avoiding hard failures in production if configured.
Proposed behavior: