-
Notifications
You must be signed in to change notification settings - Fork 11.4k
[12.x] Event Lifecycle Hooks #56150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 12.x
Are you sure you want to change the base?
[12.x] Event Lifecycle Hooks #56150
Conversation
Thanks for submitting a PR! Note that draft PR's are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface. Pull requests that are abandoned in draft may be closed due to inactivity. |
88eadfc
to
0b03152
Compare
a339fb2
to
b26ec50
Compare
b26ec50
to
d4bcdb7
Compare
I'd only have dispatcher middleware. IMO, it aligns best with what we already have for routes and jobs. The "messiness" argument can be solved by registering multiple middleware specific to a particular event and returning earlier if the event is not the desired one. The example given to justify the "messiness" handles multiple events on a single middleware, while registering a hook for each event. One could argue that having a hook for all events (e.g. Also, IMO, a middleware allows for a finer grained control, and also to have the same unit handling before/after and failures for example: class MyEventMiddleware
{
public function handle($event, $next)
{
if ($event instanceof MyEvent) {
// return earlier for other events
return $next($event);
}
// do something "before"
// ...
try {
$result = $next($event);
} catch (\Throwable $exception) {
// do something for a failure
// ...
return false;
}
// do something "after"
// ...
return $result;
}
} |
This is precisely the type of convoluted conditional logic that hooks help to avoid. Yes, you could do that, but would you really want to? It's not an approach that scales cleanly to more than a couple of events.
Indeed it could. I wouldn't suggest going "all in" on wildcard callbacks any more than I would suggest using middleware for event-specific concerns. Two different tools for two different use cases.
I don't disagree, and for cross-cutting concerns, that's the approach that I would take as well. I'd prefer, however, not to have to shoehorn non-cross-cutting concerns into something that is applied to every dispatch, nor have to maintain extensive conditional logic or countless one-off middleware. |
What would be the difference between registering a middleware per event, and a hook per event? I think I didn't express myself clearly, but that was the comparison I wanted to make, when comparing having multiple middleware per event to a catch-all ( Mind that I am not a maintainer, and we have before/after hooks for policies. So it is up to Taylor and the crew to decide on that. I just don't see a clear advantage of adding hooks over middleware for the event's dispatcher, as it seems similar to how the route's dispatcher and the job's dispatcher already implement. |
6df12a5
to
dede19b
Compare
029dd27
to
1573feb
Compare
4743d11
to
0ce34cc
Compare
0031986
to
dd5c49f
Compare
74a17f5
to
e1a1269
Compare
Maintaining and reasoning about the code. Middleware that is conditionally applied to only specific events will still run on every event, they just won't do anything. Event lifecycle hook callbacks will only be invoked against the events they are registered for. This allows you to register countless callbacks, but only have the expected ones actually invoked, making it much easier to trace in code than sifting through a massive middleware stack, 90% of which aren't applicable to your event (yet are being invoked anyway). |
e1a1269
to
668c5e3
Compare
💡 Description
This pull request introduces event lifecycle hooks -- the ability to register callbacks to be run before and after event dispatch, as well as on listener failure -- enabling developers to implement cross-cutting concerns like logging, metrics, validation, and error handling in a clean, reusable way across event-driven applications.
+ Primary Functionality
before
hook allows callbacks to be registered against one or more (or all) events, to be run before the event is dispatched to listeners.after
hook allows callbacks to be registered against one or more (or all) events, to be run after all listeners have completed successfully.failure
hook allows callbacks to be registered against one or more (or all) events, to be run on listener failure.+ Additional Functionality
EventPropagationException
can be thrown in abefore
callback to prevent dispatch to subsequent callbacks or listeners.before
callbacks are invoked in FIFO order, those registered as wildcards (all events), followed by those registered against the specific event, and finally those implemented on the event itself, whileafter
andfailure
are invoked in LIFO order those implemented on the event itself, followed by those registered against the specific event, and finally those registered as wildcards (all events).🏗️ Usage
⚖️ Justification
?Event Hooks or Listeners?
Event lifecycle hooks solve a different problem than listeners. Listeners are about what happens when an event occurs, focused upon the business logic and domain-specific responses to the event. Hooks are about how the event system itself behaves for that event, focused upon the infrastructure and the operational concerns that should happen regardless of which specific listeners are registered.
Consider an example:
A number of issues arise when attempting to implement infrastructural concerns as listeners:
For example:
Event lifecycle hooks enable framework-level features that can't be as cleanly implemented via listeners, for instance:
Package authors can also leverage event lifecycle hooks to provide infrastructure without interfering with application logic, as in the following:
Not only do event lifecycle hooks and listeners peacefully coexist, but the result of using them in concert is cleaner separation of concerns, more reliable infrastructure code, and packages that can enhance the event system without stepping on application logic.
?Event Hooks or Dispatcher Middleware?
Event lifecycle hooks also solve a different problem than dispatcher middleware. Dispatcher middleware, as the name implies, operates at the dispatcher level, wrapping the entire dispatch process. Event lifecycle hooks, on the other hand, operate at the event class level, tied to specific event types. This creates two distinct layers of control that solve different problems.
Consider an example:
The following illustrates how the two systems compose beautifully:
Middleware is about the system, while hooks are about the events. Trying to force one to do the other's job results in either bloated middleware full of conditionals, or a lack of system-wide control, as in this example:
Using dispatcher middleware for cross-cutting infrastructural event concerns and event lifecycle hooks for those that are event-specific simply feels intuitive:
Complementary usage of event lifecycle hooks and dispatcher middleware can yield elegant package integration patterns:
As with listeners, not only is there a peaceful coexistence between event lifecycle hooks and dispatcher middleware, their use together provides fine-grain control of event infrastructural concerns while avoiding awkward conditional logic, and provides for more robust package integration patterns.
!Conclusion
This feature follows Laravel's layered approach, with each layer having clear responsibilities which doesn't step on the others:
Laravel's strength has always been providing the right tool for each level of abstraction; Event lifecycle hooks complete the event system's architecture by giving developers event-specific control that complements, rather than competes with, both listeners and dispatcher middleware.
🪵 Changelog
getContainer()
.EventHooks
trait, enhancedinvokeListeners()
to trigger event lifecycle callbacks when present, replaced container calls withgetContainer()
.before()
,after()
, andfailure()
as well ascallbacks()
.EventHooks
andDispatcher
integration, covering registration, invocation, and error handling.♻ Backward Compatibility
The feature is 100% opt-in and fully backward compatible, introducing no breaking changes:
Dispatcher::invokeListeners()
), the code is entirely additive; the behavior ofDispatcher::invokeListeners()
is unchanged if the feature is not utilized.💥 Performance
Several mechanisms have been included to minimize any performance impact of the feature:
hasCallbacks()
is utilized at the integration point inDispatcher::invokeListeners()
to skip the more expensive callback aggregation when none exist for the hook/event.prepareCallbacks()
,aggregateCalllbacks()
,aggregateHierarchicalCallbacks()
,hierachicalEvents()
,callbacksForEvent()
, andcallbacksForHookAndEvent()
.hasCallbacks()
andhasHierarchicalCallbacks()
.✅ Testing
A robust set of tests is included with the feature:
✓ Callback Registration
✓ Callback Aggregation
✓ Callback Invocation
✓ Event Propagation
✓ Callback Validation
✓ Callback Detection
✓ Caching & Memoization
⚙️ Behavior
🔮 Future Possibilities
Event
facade allowing simpler testing of event lifecycle callbacks.EventPropagationException
is thrown.Advanced Callback Registration -- the ability to register callbacks not only by event name, but also by event classname (even when it differs from the the event name), as well as any classnames the event extends or implements.(since implemented)💁🏼♂️ The author is available for hire -- inquire at [email protected].