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.
  • Event lifecycle callbacks may be registered against an event name string or an event class name.
  • Event lifecycle callbacks may also be registered for the class name of a parent class or interface; a callback registered for a parent event class will be triggered for any child event classes, and a callback registered for an interface will be triggered by any event that implements the interface.
  • Class-based event lifecycle callbacks are resolved via the container, allowing dependencies to be auto-injected into the constructor.

🏗️ Usage

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

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

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

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

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

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

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

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

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

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

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

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

// 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(
    SomeEvent::class,
    function ($event) {
        if ($condition) {
            throw new Event PropagationException;
        }
    }
);

// register an event lifecycle callback for all events extending a parent
Event::before(AbstractParent::class, function ($event) {/* action */});

// register an event lifecycle callback for all event implementing an interface
Event::before(SomeInterface::class, function ($event) {/* action */});

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

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

// 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
$dispatcher->callbacks(SomeEvent::class);

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

// 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);

⚖️ 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:

// business logic: belongs in listeners
class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event)
    {
        Mail::to($event->user)->send(new WelcomeEmail());
    }
}

// infrastructural concerns: belongs in hooks
Event::before(
    UserRegistered::class,
    function($event) {
        // should happen regardless of what listeners exist
        RateLimiter::hit('user-registration:' . request()->ip());
    }
);

Event::after(
    UserRegistered::class,
    function($event) {
        // should happen after all listeners complete
        Metrics::timing('user.registration.total_time', $this->processingTime);
    }
);

A number of issues arise when attempting to implement infrastructural concerns as listeners:

  • Order Dependency Complications: "Cleanup" listener must be registered last, "setup" listener must be first, creating fragile coupling between unrelated concerns.
  • Lack of Failure Guarantees: If a business logic listener fails, monitoring/cleanup listeners never run. Hooks guarantee infrastructure code runs regardless of business logic failures.
  • Leaky Abstractions: Domain events become polluted with infrastructure listeners which aren't really about the domain event itself.

For example:

// with listeners - fragile and incomplete
class PerformanceMonitoringServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // how do you ensure this runs first?
        Event::listen('*', StartTimingListener::class);
        // how do you ensure this runs last?
        Event::listen('*', EndTimingListener::class);
        // what if a listener fails?
    }
}

// with hooks - robust and complete
class PerformanceMonitoringServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Event::before(fn($event) => $this->startTiming($event));
        Event::after(fn($event) => $this->endTiming($event));
        Event::failure(fn($event, $exception) => $this->recordFailure($event, $exception));
    }
}

Event lifecycle hooks enable framework-level features that can't be as cleanly implemented via listeners, for instance:

// :

// telescope integration
Event::before(function($event) { Telescope::startRecording($event); });
Event::after(function($event) { Telescope::stopRecording($event); });

// queue failure handling
Event::failure(
    QueuedEvent::class,
    function($event, $exception) {
        if ($exception instanceof MaxAttemptsExceededException) {
            FailedJobNotification::dispatch($event, $exception);
        }
    }
);

Package authors can also leverage event lifecycle hooks to provide infrastructure without interfering with application logic, as in the following:

// a monitoring package
Event::before(fn($event) => Monitor::start($event);
Event::after(fn($event) => Monitor::end($event));

// a caching package  
Event::after(CachedModel::class, fn($e) => Cache::invalidateModel($event->model));

// a security package
Event::before(fn($event) => SecurityLog::record($event));

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:

// dispatcher middleware: "how should all event dispatching behave?"
class EventSecurityMiddleware {
    public function handle($event, $next)
    {
        SecurityLog::startEventDispatch($event);
        
        try {
            $result = $next($event);
            SecurityLog::eventDispatchSuccess($event);
            return $result;
        } catch (\Exception $exception) {
            SecurityLog::eventDispatchFailure($event, $exception);
            throw $exception;
        }
    }
}

// event lifecycle hooks: "how should this specific event type behave?"
Event::before(
    PaymentProcessed::class,
    function($event) {
        if ($event->amount > 10000) {
            FraudDetection::flagForReview($event);
        }
    }
);

The following illustrates how the two systems compose beautifully:

// dispatcher middleware
Event::middleware([
    SecurityMiddleware::class,
    PerformanceMiddleware::class,
    LoggingMiddleware::class,
]);

// event lifecycle hooks
Event::before(SensitiveDataAccessed::class, fn($event) => AuditTrail::record($event));
Event::after(CacheCleared::class, fn($event) => Artisan::call('route:cache'));
Event::failure(CriticalProcess::class, fn($event, $exception) => PagerDuty::alert($event));

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:

// dispatcher middleware trying to do event-specific work: messy
class EventMiddleware
{
    public function handle($event, $next)
    {
        if ($event instanceof UserRegistered) {
            RateLimiter::hit('registration:' . request()->ip());
        } elseif ($event instanceof PaymentProcessed) {
            if ($event->amount > 1000) {
                AuditLog::record($event);
            }
        } elseif ($event instanceof OrderShipped) {
            // ...more conditions
        }
        
        $result = $next($event);
        
        if ($event instanceof UserRegistered) {
            Metrics::increment('user.registrations');
        }
        
        return $result;
    }
}

// event lifecycle hooks: clean & declarative
Event::before(
    UserRegistered::class,
    fn($event) => RateLimiter::hit('registration:' . request()->ip())
);
Event::before(
    PaymentProcessed::class,
    fn($event) => $event->amount > 1000 ? AuditLog::record($event) : null
);
Event::after(
    UserRegistered::class
    fn($event) => Metrics::increment('user.registrations')
);

Using dispatcher middleware for cross-cutting infrastructural event concerns and event lifecycle hooks for those that are event-specific simply feels intuitive:

// dispatcher middleware: universal concerns that apply to all events
class PerformanceMiddleware {
    public function handle($event, $next)
    {
        $start = microtime(true);
        
        try {
            $result = $next($event);
            Metrics::timing('events.dispatch_time', microtime(true) - $start);
            return $result;
        } catch (\Exception $e) {
            Metrics::increment('events.dispatch_failures');
            throw $e;
        }
    }
}

// event lifecycle hooks: event-specific behavior
Event::before(
    UserRegistered::class,
    function($event) {
        // only relevant to user registration
        if (User::where('email', $event->user->email)->count() > 1) {
            throw new DuplicateRegistrationException();
        }
    }
);

Event::after(
    OrderShipped::class,
    function($event) {
        // only relevant to order shipping
        $event->order->update(['tracking_sent_at' => now()]);
    }
);

Complementary usage of event lifecycle hooks and dispatcher middleware can yield elegant package integration patterns:

// a monitoring package uses dispatcher middleware for universal metrics
class MonitoringMiddleware
{
    public function handle($event, $next)
    {
        Monitor::eventStarted(get_class($event));
        $result = $next($event);
        Monitor::eventCompleted(get_class($event));
        return $result;
    }
}

// but uses event lifecycle hooks for event-specific insights
Event::after(DatabaseQueryExecuted::class, fn($event) => Monitor::queryTime($event->time));
Event::failure(JobFailed::class, fn($event, $exception) => Monitor::jobFailure($event->job, $exception));

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:

  • Dispatcher Middleware
    • Defines cross-cutting event dispatch behavior.
    • Answers the question: “How should the event dispatching process behave?”.
  • Event Lifecycle Hooks
    • Defines event-specific infrastructural behavior.
    • Answers the question: ”How should the event system process this event?”.
  • Event Listeners
    • Defines event-specific logical behavior.
    • Answers the question: “What business process should happen when this event occurs?.”

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

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 its 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

Several mechanisms have been included to minimize any performance impact of the feature:

  • 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 prepareCallbacks(), aggregateCalllbacks(), aggregateHierarchicalCallbacks(), hierachicalEvents(), callbacksForEvent(), and callbacksForHookAndEvent().
  • Memoization via a class array is utilized to enhance the performance of hasCallbacks() and hasHierarchicalCallbacks().

✅ Testing

A robust set of tests is included with the feature:

✓ 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
  • test_registered_hierarchical_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_hierarchical_callbacks_are_called_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

✓ Callback Validation

  • 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

  • test_callback_detection_recognizes_wildcard_callbacks
  • test_callback_detection_recognizes_event_specific_callbacks
  • test_callback_detection_recognizes_hierarchical_parent_callbacks
  • test_callback_detection_recognizes_hierarchical_interface_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

✓ Caching & Memoization

  • test_has_callbacks_utilizes_memoization_correctly
  • test_aggregated_callbacks_are_cached
  • test_aggregated_callbacks_are_read_from_cache
  • test_hook_and_event_callbacks_are_cached
  • test_hook_and_event_callbacks_are_read_from_cache
  • test_event_callbacks_are_cached
  • test_event_callbacks_are_read_from_cache
  • test_event_hierarchies_are_cached
  • test_event_hierarchies_are_read_from_cache
  • test_hierarchical_callbacks_are_cached
  • test_hierarchical_callbacks_are_read_from_cache
  • test_prepared_callbacks_are_cached
  • test_prepared_callbacks_are_read_from_cache

⚙️ 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).
  • Artisan Command -- list event lifecycle callback registrations.
  • Testing Enhancements -- assertions on the Event facade allowing simpler testing of event lifecycle callbacks.
  • Callback Groups -- the ability to group callbacks to be invoked together, even if an 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].

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
@yitzwillroth yitzwillroth marked this pull request as draft June 27, 2025 17:13
@yitzwillroth yitzwillroth force-pushed the add-event-hooks branch 2 times, most recently from a339fb2 to b26ec50 Compare June 27, 2025 18:01
@yitzwillroth yitzwillroth marked this pull request as ready for review June 27, 2025 18:12
@yitzwillroth yitzwillroth marked this pull request as draft June 27, 2025 20:31
@rodrigopedra
Copy link
Contributor

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. "*"), could lead to the same if/else mess.

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;
    }
}

@yitzwillroth
Copy link
Contributor Author

yitzwillroth commented Jun 29, 2025

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.

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.

One could argue that having a hook
for all events (e.g. "*"), could lead to
the same if/else mess.

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.

Also, IMO, a middleware allows for a
finer grained control, and also to have
the same unit handling before/after
and failures for example:

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.

@rodrigopedra
Copy link
Contributor

This is precisely the type of convoluted conditional logic that hooks help to avoid.

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 ("*") hook.

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.

@yitzwillroth yitzwillroth force-pushed the add-event-hooks branch 2 times, most recently from 6df12a5 to dede19b Compare June 29, 2025 14:17
@yitzwillroth yitzwillroth marked this pull request as ready for review June 29, 2025 14:29
@yitzwillroth yitzwillroth force-pushed the add-event-hooks branch 4 times, most recently from 029dd27 to 1573feb Compare June 29, 2025 23:24
@yitzwillroth yitzwillroth marked this pull request as draft June 29, 2025 23:28
@yitzwillroth yitzwillroth force-pushed the add-event-hooks branch 10 times, most recently from 4743d11 to 0ce34cc Compare June 30, 2025 15:37
@yitzwillroth yitzwillroth force-pushed the add-event-hooks branch 2 times, most recently from 0031986 to dd5c49f Compare July 1, 2025 19:02
@yitzwillroth yitzwillroth marked this pull request as ready for review July 1, 2025 21:55
@yitzwillroth yitzwillroth force-pushed the add-event-hooks branch 2 times, most recently from 74a17f5 to e1a1269 Compare July 2, 2025 00:19
@yitzwillroth
Copy link
Contributor Author

What would be the difference between
registering a middleware per event,
and a hook per event?

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).

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.

2 participants