6.x - Add attribute-based event listener registration#19469
Conversation
There was a problem hiding this comment.
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) andAttributeEventListenerConnectorto resolve and register listeners fromAttributeResolver. - Adds
EventManager::attachAttributes()as a convenience wrapper around the connector. - Adds a dedicated
EventAttributeExceptionplus 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.
…utes to interface, tighten docblocks
|
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 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. |
Refs RFC #19297
Introduces a new
#[EventListener]PHP attribute allowing listener methods (or classes) todeclare event subscriptions directly in code, without manual
EventManager::on()calls or implementing
EventListenerInterface.The
EventManager::registerAttributeListeners()method (orAttributeEventListenerConnectordirectly) reads attribute metadata via the existing
AttributeResolverinfrastructure — the same scan-and-cache system used for attribute-based routing —
and registers the discovered callables.
Usage
Prerequisites
registerAttributeListeners()relies on theAttributeResolverbeing configured with thepaths to scan. Follow the AttributeResolver configuration docs
to set this up — typically a
'Listener/**/*.php'glob in yourdefaultconfig.1. Activate in your Application
Override
events()in yourApplicationclass (or aPlugin):2. Declare listeners with the attribute
Method-level (most common — the decorated method is the callable):
Class-level with explicit method:
Class-level with invokable class:
Class-level with inferred method name (
Order.afterPlace→onOrderAfterPlace):Callable resolution order (class-level attributes)
methodargument on the attribute__invoke()when present on the classOrder.afterPlace→onOrderAfterPlace)Abstract classes, interfaces and traits are silently skipped. Non-public or
missing methods throw
EventAttributeExceptionwith a descriptive message.