Skip to content

[12.x] Deferred Events #56556

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

Merged
merged 3 commits into from
Aug 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/Illuminate/Events/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ class Dispatcher implements DispatcherContract
*/
protected $transactionManagerResolver;

/**
* The currently deferred events.
*
* @var array
*/
protected $deferredEvents = [];

/**
* Indicates if events should be deferred.
*
* @var bool
*/
protected $deferringEvents = false;

/**
* Create a new event dispatcher instance.
*
Expand Down Expand Up @@ -244,6 +258,12 @@ public function until($event, $payload = [])
*/
public function dispatch($event, $payload = [], $halt = false)
{
if ($this->deferringEvents) {
$this->deferredEvents[] = func_get_args();

return null;
}

// When the given "event" is actually an object we will assume it is an event
// object and use the class as the event name and this event itself as the
// payload to the handler, which makes object based events quite simple.
Expand Down Expand Up @@ -768,6 +788,36 @@ public function setTransactionManagerResolver(callable $resolver)
return $this;
}

/**
* Execute the given callback while deferring events, then dispatch all deferred events.
*
* @param callable $callback
* @return mixed
*/
public function defer(callable $callback)
{
$wasDeferring = $this->deferringEvents;
$previousDeferredEvents = $this->deferredEvents;

$this->deferringEvents = true;
$this->deferredEvents = [];

try {
$result = $callback();

$this->deferringEvents = false;

foreach ($this->deferredEvents as $args) {
$this->dispatch(...$args);
}

return $result;
} finally {
$this->deferringEvents = $wasDeferring;
$this->deferredEvents = $previousDeferredEvents;
}
}

/**
* Gets the raw, unprepared listeners.
*
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* @method static \Illuminate\Events\Dispatcher setQueueResolver(callable $resolver)
* @method static \Illuminate\Events\Dispatcher setTransactionManagerResolver(callable $resolver)
* @method static array getRawListeners()
* @method static mixed defer(callable $callback)
* @method static void macro(string $name, object|callable $macro)
* @method static void mixin(object $mixin, bool $replace = true)
* @method static bool hasMacro(string $name)
Expand Down
61 changes: 61 additions & 0 deletions tests/Events/EventsDispatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,67 @@ public function testBasicEventExecution()
$this->assertSame('barbar', $_SERVER['__event.test']);
}

public function testDeferEventExecution()
{
unset($_SERVER['__event.test']);
$d = new Dispatcher;
$d->listen('foo', function ($foo) {
$_SERVER['__event.test'] = $foo;
});

$result = $d->defer(function () use ($d) {
$d->dispatch('foo', ['bar']);
$this->assertArrayNotHasKey('__event.test', $_SERVER);

return 'callback_result';
});

$this->assertEquals('callback_result', $result);
$this->assertSame('bar', $_SERVER['__event.test']);
}

public function testDeferMultipleEvents()
{
$_SERVER['__event.test'] = [];
$d = new Dispatcher;
$d->listen('foo', function ($value) {
$_SERVER['__event.test'][] = $value;
});
$d->listen('bar', function ($value) {
$_SERVER['__event.test'][] = $value;
});
$d->defer(function () use ($d) {
$d->dispatch('foo', ['foo']);
$d->dispatch('bar', ['bar']);
$this->assertSame([], $_SERVER['__event.test']);
});

$this->assertSame(['foo', 'bar'], $_SERVER['__event.test']);
}

public function testDeferNestedEvents()
{
$_SERVER['__event.test'] = [];
$d = new Dispatcher;
$d->listen('foo', function ($foo) {
$_SERVER['__event.test'][] = $foo;
});

$d->defer(function () use ($d) {
$d->dispatch('foo', ['outer1']);

$d->defer(function () use ($d) {
$d->dispatch('foo', ['inner']);
$this->assertSame([], $_SERVER['__event.test']);
});

$this->assertSame(['inner'], $_SERVER['__event.test']);
$d->dispatch('foo', ['outer2']);
});

$this->assertSame(['inner', 'outer1', 'outer2'], $_SERVER['__event.test']);
}

public function testHaltingEventExecution()
{
unset($_SERVER['__event.test']);
Expand Down
96 changes: 96 additions & 0 deletions tests/Integration/Events/DeferEventsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Illuminate\Tests\Integration\Events;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Event;
use Orchestra\Testbench\TestCase;

class DeferEventsTest extends TestCase
{
public function testDeferEvents()
{
unset($_SERVER['__event.test']);

Event::listen('foo', function ($foo) {
$_SERVER['__event.test'] = $foo;
});

$response = Event::defer(function () {
Event::dispatch('foo', ['bar']);

$this->assertArrayNotHasKey('__event.test', $_SERVER);

return 'callback_result';
});

$this->assertEquals('callback_result', $response);
$this->assertSame('bar', $_SERVER['__event.test']);
}

public function testDeferModelEvents()
{
$_SERVER['__model_event.test'] = [];

TestModel::saved(function () {
$_SERVER['__model_event.test'][] = 'saved';
});

$response = Event::defer(function () {
$model = new TestModel();
$model->fireModelEvent('saved', false);

$this->assertSame([], $_SERVER['__model_event.test']);

return 'model_event_response';
});

$this->assertEquals('model_event_response', $response);
$this->assertContains('saved', $_SERVER['__model_event.test']);
}

public function testDeferMultipleModelEvents()
{
$_SERVER['__model_events'] = [];

TestModel::saved(function () {
$_SERVER['__model_events'][] = 'saved:TestModel';
});

AnotherTestModel::created(function () {
$_SERVER['__model_events'][] = 'created:AnotherTestModel';
});

$response = Event::defer(function () {
$model1 = new TestModel();
$model1->fireModelEvent('saved');

$model2 = new AnotherTestModel();
$model2->fireModelEvent('created');

// Events should not have fired yet
$this->assertSame([], $_SERVER['__model_events']);

return 'multiple_models_response';
});

$this->assertEquals('multiple_models_response', $response);
$this->assertSame(['saved:TestModel', 'created:AnotherTestModel'], $_SERVER['__model_events']);
}
}

class TestModel extends Model
{
public function fireModelEvent($event, $halt = true)
{
return parent::fireModelEvent($event, $halt);
}
}

class AnotherTestModel extends Model
{
public function fireModelEvent($event, $halt = true)
{
return parent::fireModelEvent($event, $halt);
}
}
Loading