Skip to content

6.x - Add attribute-based event listener registration#19469

Open
josbeir wants to merge 3 commits into
6.xfrom
6.x-event-attributes
Open

6.x - Add attribute-based event listener registration#19469
josbeir wants to merge 3 commits into
6.xfrom
6.x-event-attributes

Conversation

@josbeir
Copy link
Copy Markdown
Contributor

@josbeir josbeir commented May 25, 2026

Refs RFC #19297

Introduces a new #[EventListener] PHP attribute allowing listener methods (or classes) to
declare event subscriptions directly in code, without manual EventManager::on()
calls or implementing EventListenerInterface.

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 { ... }
}

The EventManager::registerAttributeListeners() method (or AttributeEventListenerConnector
directly) reads attribute metadata via the existing AttributeResolver
infrastructure — the same scan-and-cache system used for attribute-based routing —
and registers the discovered callables.

Usage

Prerequisites

registerAttributeListeners() relies on the AttributeResolver being configured with the
paths to scan. Follow the AttributeResolver configuration docs
to set this up — typically a 'Listener/**/*.php' glob in your default config.

1. Activate in your Application

Override events() in your Application class (or a Plugin):

// src/Application.php
use Cake\Event\EventManagerInterface;

public function events(EventManagerInterface $eventManager): EventManagerInterface
{
    $eventManager->registerAttributeListeners();      // scans & registers all #[EventListener] attributes

    return $eventManager;
}

2. Declare listeners with the attribute

Method-level (most common — the decorated method is the callable):

use Cake\Event\Attribute\EventListener;

class OrdersListener
{
    #[EventListener('Order.afterPlace')]
    #[EventListener('Order.afterCancel', priority: 20)]
    public function updateMetrics(EventInterface $event): void { ... }
}

Class-level with explicit method:

#[EventListener('Order.afterPlace', method: 'handleOrder')]
class OrdersListener
{
    public function handleOrder(EventInterface $event): void { ... }
}

Class-level with invokable class:

#[EventListener('Order.afterPlace')]
class SendReceiptListener
{
    public function __invoke(EventInterface $event): void { ... }
}

Class-level with inferred method name (Order.afterPlaceonOrderAfterPlace):

#[EventListener('Order.afterPlace')]
class OrdersListener
{
    public function onOrderAfterPlace(EventInterface $event): void { ... }
}

Callable resolution order (class-level attributes)

  1. Explicit method argument on the attribute
  2. __invoke() when present on the class
  3. Method name inferred from the event (Order.afterPlaceonOrderAfterPlace)

Abstract classes, interfaces and traits are silently skipped. Non-public or
missing methods throw EventAttributeException with a descriptive message.

Copilot AI review requested due to automatic review settings May 25, 2026 15:48
@josbeir josbeir changed the title Initial work on attribute-based event listener registration Attribute-based event listener registration May 25, 2026
@josbeir josbeir changed the title Attribute-based event listener registration Add attribute-based event listener registration May 25, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class, attribute-driven event listener registration to CakePHP’s event system via a new #[EventListener] attribute and a connector that scans AttributeResolver collections and attaches discovered listeners to an EventManager.

Changes:

  • Introduces Cake\Event\Attribute\EventListener (repeatable, class/method targets) and AttributeEventListenerConnector to resolve and register listeners from AttributeResolver.
  • Adds EventManager::attachAttributes() as a convenience wrapper around the connector.
  • Adds a dedicated EventAttributeException plus comprehensive unit tests and TestApp listener fixtures.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Event/Attribute/EventListener.php Defines the new #[EventListener] attribute and its parameters.
src/Event/AttributeEventListenerConnector.php Implements scanning, callable resolution, validation, and registration logic for attribute listeners.
src/Event/EventManager.php Adds attachAttributes() helper API on the concrete event manager.
src/Event/Exception/EventAttributeException.php Introduces a specific exception type for invalid attribute declarations.
tests/TestCase/Event/AttributeEventListenerConnectorTest.php Verifies method/class-level resolution, invocation, priority, skipping abstract types, and error cases.
tests/test_app/TestApp/Event/Listener/OrdersListener.php Fixture for method-level and repeatable attributes.
tests/test_app/TestApp/Event/Listener/PriorityListener.php Fixture for priority ordering.
tests/test_app/TestApp/Event/Listener/InvokableListener.php Fixture for class-level __invoke resolution.
tests/test_app/TestApp/Event/Listener/InferredMethodListener.php Fixture for inferred method name resolution.
tests/test_app/TestApp/Event/Listener/ClassLevelMethodListener.php Fixture for explicit class-level method: resolution.
tests/test_app/TestApp/Event/Listener/MissingMethodListener.php Fixture for missing method error handling.
tests/test_app/TestApp/Event/Listener/NonPublicMethodListener.php Fixture for non-public method error handling.
tests/test_app/TestApp/Event/Listener/AbstractListener.php Fixture for skipping abstract declaring types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Event/AttributeEventListenerConnector.php
Comment thread src/Event/EventManager.php
Comment thread src/Event/AttributeEventListenerConnector.php
Comment thread src/Event/Attribute/EventListener.php Outdated
@josbeir josbeir changed the title Add attribute-based event listener registration 6.x - Add attribute-based event listener registration May 25, 2026
Comment thread src/Event/EventManagerInterface.php Outdated
Comment thread src/Event/EventManager.php Outdated
@josbeir
Copy link
Copy Markdown
Contributor Author

josbeir commented May 26, 2026

Just for discussion sake, not sure this should be part of this initial PR.

As mentioned in the RFC's comments: this implementation attaches its discovered listeners to the event manager via registerAttributeListeners which will probably be the global one for most use cases.

Using multiple manager instances is currently not possible, but we could make this more flexible by adding a resolver system to the connector.

#[EventListener('Order.afterPlace', manager: 'orders')]
public function sendReceipt(EventInterface $event): void {}

Adding multiple managers could be as simple as

new AttributeEventListenerConnector(
    $defaultManager,
    managerResolver: fn (?string $manager): EventManagerInterface => match ($manager) {
        null => $defaultManager,
        'orders' => $ordersTable->getEventManager(),
        'cache' => $cacheEngine->getEventManager(),
        default => throw new EventAttributeException(...),
    },
);

A more 'understandable' and realistic shape could also be something like this:

$ordersTable->getEventManager()->registerAttributeListenersFor('Orders');

which register all listeners marked for Orders onto this event manager.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants