-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Description
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()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
AttributeResolverfor discovery, filtering, and caching. - Keep behavior deterministic and aligned with
EventManagersemantics. - 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:
- Listener registration and dispatch ordering are implemented in src/Event/EventManager.php.
- Subscriber array contract is defined in src/Event/EventListenerInterface.php.
- HTTP lifecycle registration path calls
events()thenpluginEvents()in src/Http/BaseApplication.php. - CLI lifecycle registration path calls
events()andpluginEvents()in src/Console/CommandRunner.php. - Attribute discovery and caching primitives are provided by src/AttributeResolver/AttributeResolver.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:
public function __construct(
string $event,
int $priority = EventManager::$defaultPriority,
?string $method = null,
) {}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:
- one method to listen to multiple events.
- one class to declare multiple listeners.
Method resolution rules:
- If
methodis 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 examplefoo=>onFoo.
- use
- 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)andwithTargetType(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 (
stringkeys). - 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
EventManagerbehavior. - 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.