Skip to content

RFC - 6.0 - Attribute-Based Event Listeners #19297

@josbeir

Description

@josbeir

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:

  1. If method is provided, use it.
  2. For method-level attributes without method, use the declaring method.
  3. For class-level attributes without method:
    • use __invoke() if present.
    • otherwise, infer method names (on + normalized event identifier), for example foo => onFoo.
  4. 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:

  1. withAttribute(EventListener::class)
  2. withTargetType(AttributeTargetType::METHOD) and withTargetType(AttributeTargetType::CLASS_TYPE)
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions