Skip to content

[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

Open
wants to merge 1 commit into
base: 12.x
Choose a base branch
from

Conversation

yitzwillroth
Copy link
Contributor

@yitzwillroth yitzwillroth commented Jun 26, 2025

💡 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

  • The before hook allows callbacks to be registered against one or more (or all) events, to be run before the event is dispatched to listeners.
  • The after hook allows callbacks to be registered against one or more (or all) events, to be run after all listeners have completed successfully.
  • The failure hook allows callbacks to be registered against one or more (or all) events, to be run on listener failure.

Additional Functionality

  • The EventPropagationException can be thrown in a before callback to prevent dispatch to subsequent callbacks or listeners.
  • Event lifecycle callbacks may be callables or may be implemented directly on the event class.
  • Event lifecycle callbacks are invoked in an order mimicking the familiar pipeline ordering -- 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, while after and failure 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).
  • Event lifecycle callbacks respect event dispatch on transaction commit where indicated.
  • Event lifecycle callbacks may be registered for both domain and framework events.

🏗️ Usage

// register event lifecycle callbacks using the Event facade
Event::before(function () {/* action */}, SomeEvent::class)
    ->after(function () {/* action */}, SomeEvent::class)
    ->failure(function () {/* action */}, SomeEvent::class);

// register event lifecycle callbacks directly on the dispatcher
$dispatcher->before(function () {/* action */}, SomeEvent::class)
    ->after(function () {/* action */}, SomeEvent::class)
    ->failure(function () {/* action */}, SomeEvent::class);

// register a callback to execute before listener dispatch
$dispatcher->before(function () {/* action */}, SomeEvent::class);

// register a callback to execute after listener execution
$dispatcher->after(function () {/* action */}, SomeEvent::class);

// register a callback to execute on listener failure
$dispatcher->failure(function () {/* action */}, SomeEvent::class);

// register multiple event lifecycle callbacks for a single event
$dispatcher->before([
    function () {/* action */},
    function () {/* action */},
    function () {/* action */},
], SomeEvent::class);

// register an event lifecycle callback for multiple events
$dispatcher->
	failure(function () {
	    // do something
	}, [
	Event1::class,
 	Event1::class,
]);

// register multiple event lifecycle callbacks for multiple events
$dispatcher->before([
    function () {/* action */},
    function () {/* action */},
    function () {/* action */},
], [
	Event1::class,
 	Event1::class,
]);

// register an event lifecycle callback for all events
$dispatcher->before(function () {/* action */});

// register multiple event lifecycle callbacks for all events
$dispatcher->before([
    function () {/* action */},
    function () {/* action */},
    function () {/* action */},
]);

// register event lifecycle callbacks using various event formats
$dispatcher->before(function () {/* action */}, Event::class);
$dispatcher->before(function () {/* action */}, 'event.happened');

// register event lifecycle callbacks using various callback formats
$dispatcher->before(function () {/* action */}, Event::class);
$dispatcher->before(someFunction(...), Event::class);
$dispatcher->before(SomeClass::class, Event::class); // handle or __invoke
$dispatcher->before(SomeClass::class.'::method', Event::class);
$dispatcher->before(SomeClass::class.'@method', Event::class);

// implement event lifecycle callbacks as a class
class DoSomethingBefore
{
    public function __invoke(object $event): void
    {
        // do something
    }
}

// implement event lifecycle callbacks directly on the event
class SomeEvent
{
    public function before(object $event): void
    {
        // do something
    }
}

// halt propagation to subsequent callbacks and listener dispatch
Event::before(function () {
    if ($condition) {
        throw new Event PropagationException;
    }
}, SomeEvent::class);

// get all callbacks registered with the dispatcher
$dispatcher->callbacks();

// get all callbacks registered with the dispatcher for an event
$dispatcher->callbacks(null, static::HOOK:BEFORE);

// get all callbacks registered with the dispatcher for a hook
$dispatcher->callbacks(static::HOOK:BEFORE);

// get all callbacks registered with the dispatcher for an event & hook
$dispatcher->callbacks(static::HOOK:BEFORE, SomeEvent::class);

// get all wildcard callbacks registered with the dispatcher 
$dispatcher->callbacks(null, '*');

// get all wildcard callbacks registered with the dispatcher for a hook 
$dispatcher->callbacks(static::HOOK:BEFORE, '*');

// determine if an event has callbacks (registered or class implemented)
$dispatcher->hasCallbacks($event);

🪵 Changelog

File Change
src/Illuminate/Events/EventHooks.php New trait to manage before, after, and failure event lifecycle hooks, including registration, retrieval, validation, and invocation; add getContainer().
src/Illuminate/Events/Dispatcher.php Added EventHooks trait, enhanced invokeListeners() to trigger event lifecycle callbacks when present, replaced container calls with getContainer().
src/Illuminate/Support/Facades/Event.php Added method annotations for new event hooks methods -- before(), after(), and failure() as well as callbacks().
src/Illuminate/Events/EventPropagationException.php New exception allowing halting event propagation from a lifecycle callback.
tests/Illuminate/Events/EventHooksTest.php New test class for EventHooks and Dispatcher integration, covering registration, invocation, and error handling.

♻ Backward Compatibility

The feature is 100% opt-in and fully backward compatible, introducing no breaking changes:

  • With the exception of a narrow integration point (Dispatcher::invokeListeners()), the code is entirely additive; the behavior of Dispatcher::invokeListeners() is unchanged if the feature is not utilized.
  • No public interfaces are modified.
  • No public method signatures are modified.

💥 Performance

  • Fast-failure (via hasCallbacks() is utilized at the integration point in Dispatcher::invokeListeners() to skip the more expensive callback aggregation when none exist for the hook/event.
  • Caching via a class array is utilized to enhance the performance of aggregateCalllbacks().
  • Memoization via a class array is utilized to enhance the performance of hasCallbacks().

✅ Testing

A robust set of tests is included with the pull request:

✓ Callback Registration

  • test_registers_wildcard_callbacks_correctly
  • test_registers_before_callbacks_correctly
  • test_registers_after_callbacks_correctly
  • test_registers_failure_callbacks_correctly
  • test_registers_multiple_callbacks_for_single_event_correctly
  • test_registers_callbacks_for_multiple_events_correctly
  • test_registers_array_of_callbacks_for_single_event_correctly
  • test_registers_single_callback_for_array_of_events_correctly
  • test_registers_global_callbacks_correctly
  • test_registered_callbacks_can_be_accessed_correctly

✓ Callback Aggregation

  • test_callback_aggregation_includes_both_wildcard_and_specific_callbacks
  • test_callback_aggregation_caches_results
  • test_callback_aggregation_with_invalid_hook_throws_exception
  • test_callback_aggregation_includes_all_registered_callbacks
  • test_callback_aggregation_includes_event_object_hook_methods
  • test_prepares_event_object_callback_when_present
  • test_does_not_prepare_event_object_callbacks_when_hook_methods_are_missing
  • test_callbacks_are_ordered_for_before_hook_correctly
  • test_callbacks_are_ordered_for_after_hook_correctly
  • test_callbacks_are_ordered_for_failure_hook_correctly
  • test_callbacks_are_combined_and_ordered_correctly

✓ Callback Invocation

  • test_all_callbacks_are_invoked
  • test_before_callbacks_are_called_before_listeners
  • test_after_callbacks_are_called_after_listeners
  • test_failure_callbacks_are_called_on_listener_failure
  • test_invokes_object_and_method_format_callback_correctly
  • test_invokes_class_method_with_at_notation_correctly
  • test_invokes_class_method_with_double_colon_notation_correctly
  • test_invokes_class_handle_method_with_classname_notation_correctly
  • test_invokes_invokable_class_correctly
  • test_invokes_closure_callback_correctly
  • test_callbacks_receive_and_process_payload_correctly
  • test_callbacks_are_only_invoked_when_they_exist
  • test_no_callbacks_are_invoked_when_no_callbacks_exist_for_event
  • test_multiple_listeners_with_before_and_after_callbacks_execute_in_order
  • test_event_methods_are_called_in_correct_order
  • test_event_hook_methods_are_called_during_dispatch_correctly
  • test_event_methods_are_called_with_correct_payload
  • test_event_with_failure_method_is_called_when_listener_returns_false

✓ Event Propagation

  • test_event_propagation_exception_halts_callback_processing
  • test_event_propagation_exception_prevents_listener_processing

✓ Hook & Callback Validation

  • test_invalid_hook_throws_exception
  • test_invalid_callback_throws_exception
  • test_callback_with_nonexistent_class_throws_exception
  • test_callback_with_nonexistent_method_throws_exception
  • test_invoking_invalid_callback_throws_exception
  • test_validates_callback_string_with_at_notation
  • test_validates_callback_string_with_double_colon_notation

✓ Callback Detection and Memoization

  • test_callback_detection_recognizes_wildcard_callbacks
  • test_callback_detection_recognizes_event_specific_callbacks
  • test_callback_detection_returns_false_when_none_are_registered
  • test_callback_detection_recognizes_event_object_callbacks
  • test_callback_detection_ignores_objects_without_hook_methods
  • test_has_callbacks_utilizes_memoization_correctly

⚙️ Behavior

sequenceDiagram
    participant Client
    participant Dispatcher
    participant EventHooks
    participant Container
    participant BeforeCallback
    participant Listener
    participant AfterCallback
    participant FailureCallback

    Note over Client,FailureCallback: Hook Registration Phase
    Client->>Dispatcher: before(event, callback)
    Dispatcher->>EventHooks: registerBeforeHook(event, callback)
    EventHooks->>EventHooks: validateCallback(callback)
    EventHooks-->>Dispatcher: hook registered

    Client->>Dispatcher: after(event, callback)
    Dispatcher->>EventHooks: registerAfterHook(event, callback)
    EventHooks-->>Dispatcher: hook registered

    Client->>Dispatcher: failure(event, callback)
    Dispatcher->>EventHooks: registerFailureHook(event, callback)
    EventHooks-->>Dispatcher: hook registered

    Note over Client,FailureCallback: Event Dispatch Phase
    Client->>Dispatcher: dispatch(event, payload, halt)
    
    Dispatcher->>EventHooks: invokeCallbacks('before', event, payload)
    EventHooks->>Container: resolveCallback(callback)
    Container-->>EventHooks: resolved callback
    EventHooks->>BeforeCallback: invoke(event, payload)
    
    alt EventPropagationException thrown
        BeforeCallback-->>EventHooks: throw EventPropagationException
        EventHooks-->>Dispatcher: propagation stopped
        Dispatcher-->>Client: return null (aborted)
    else Normal flow continues
        BeforeCallback-->>EventHooks: callback completed
        EventHooks-->>Dispatcher: before hooks completed
        
        loop For each listener
            Dispatcher->>Listener: invoke(event, payload)
            Listener-->>Dispatcher: response
            
            alt halt=true AND response !== null
                alt response === false
                    Dispatcher->>EventHooks: invokeCallbacks('failure', event, payload)
                    EventHooks->>Container: resolveCallback(failureCallback)
                    Container-->>EventHooks: resolved callback
                    EventHooks->>FailureCallback: invoke(event, payload, response)
                    FailureCallback-->>EventHooks: callback completed
                    EventHooks-->>Dispatcher: failure hooks completed
                    Dispatcher-->>Client: return false
                else response !== false
                    Dispatcher->>EventHooks: invokeCallbacks('after', event, payload)
                    EventHooks->>Container: resolveCallback(afterCallback)
                    Container-->>EventHooks: resolved callback
                    EventHooks->>AfterCallback: invoke(event, payload, response)
                    AfterCallback-->>EventHooks: callback completed
                    EventHooks-->>Dispatcher: after hooks completed
                    Dispatcher-->>Client: return response
                end
            end
        end
        
        Note over Dispatcher,AfterCallback: All listeners completed
        Dispatcher->>EventHooks: invokeCallbacks('after', event, payload)
        EventHooks->>Container: resolveCallback(afterCallback)
        Container-->>EventHooks: resolved callback
        EventHooks->>AfterCallback: invoke(event, payload, responses)
        AfterCallback-->>EventHooks: callback completed
        EventHooks-->>Dispatcher: after hooks completed
        Dispatcher-->>Client: return responses
    end
Loading

🔮 Future Possibilities

  • Callback prioritization -- current implementation guarantees only FIFO/LIFO invocation (depending upon hook).
  • Callback batches -- the ability to group callbacks to be invoked as a group, even if an EventPropagationException is thrown.

💁🏼‍♂️ The author is available for hire -- inquire at [email protected].

Copy link

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.

@yitzwillroth yitzwillroth marked this pull request as ready for review June 26, 2025 23:18
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.

1 participant