Skip to content

Commit a750ae5

Browse files
[12.x] Fix support for adding custom observable events from traits (#55286)
* Add failing test * Resolve observer attributes after booting has finished - This ensure that all custom events have been registered by any traits first. - It also prevents a new instance of the model from being created by the call to `observe` too early, since an instance created before booting has finished would be in an inconsistent state due to not all of the trait boot methods being run yet and not all of the initializer methods would be registered yet, so they would not be run either. * Style fixes * Add tests for booted callbacks * Add `whenBooted` method to models * Use `whenBooted` instead of registering an event listener - This removes any reliance on the event dispatcher, which complicated testing. * Clear the callbacks when clearing booted models * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 912ab40 commit a750ae5

File tree

4 files changed

+138
-1
lines changed

4 files changed

+138
-1
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasEvents.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ trait HasEvents
3838
*/
3939
public static function bootHasEvents()
4040
{
41-
static::observe(static::resolveObserveAttributes());
41+
static::whenBooted(fn () => static::observe(static::resolveObserveAttributes()));
4242
}
4343

4444
/**

src/Illuminate/Database/Eloquent/Model.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Illuminate\Database\Eloquent;
44

55
use ArrayAccess;
6+
use Closure;
67
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
78
use Illuminate\Contracts\Queue\QueueableCollection;
89
use Illuminate\Contracts\Queue\QueueableEntity;
@@ -149,6 +150,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
149150
*/
150151
protected static $booted = [];
151152

153+
/**
154+
* The callbacks that should be executed after the model has booted.
155+
*
156+
* @var array
157+
*/
158+
protected static $bootedCallbacks = [];
159+
152160
/**
153161
* The array of trait initializers that will be called on each new instance.
154162
*
@@ -279,6 +287,12 @@ protected function bootIfNotBooted()
279287
static::boot();
280288
static::booted();
281289

290+
static::$bootedCallbacks[static::class] ??= [];
291+
292+
foreach (static::$bootedCallbacks[static::class] as $callback) {
293+
$callback();
294+
}
295+
282296
$this->fireModelEvent('booted', false);
283297
}
284298
}
@@ -357,6 +371,19 @@ protected static function booted()
357371
//
358372
}
359373

374+
/**
375+
* Register a closure to be executed after the model has booted.
376+
*
377+
* @param \Closure $callback
378+
* @return void
379+
*/
380+
protected static function whenBooted(Closure $callback)
381+
{
382+
static::$bootedCallbacks[static::class] ??= [];
383+
384+
static::$bootedCallbacks[static::class][] = $callback;
385+
}
386+
360387
/**
361388
* Clear the list of booted models so they will be re-booted.
362389
*
@@ -365,6 +392,7 @@ protected static function booted()
365392
public static function clearBootedModels()
366393
{
367394
static::$booted = [];
395+
static::$bootedCallbacks = [];
368396

369397
static::$globalScopes = [];
370398
}

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2293,6 +2293,35 @@ public function testModelIsBootedOnUnserialize()
22932293
$this->assertTrue(EloquentModelBootingTestStub::isBooted());
22942294
}
22952295

2296+
public function testCallbacksCanBeRunAfterBootingHasFinished()
2297+
{
2298+
$this->assertFalse(EloquentModelBootingCallbackTestStub::$bootHasFinished);
2299+
2300+
$model = new EloquentModelBootingCallbackTestStub();
2301+
2302+
$this->assertTrue($model::$bootHasFinished);
2303+
2304+
EloquentModelBootingCallbackTestStub::unboot();
2305+
}
2306+
2307+
public function testBootedCallbacksAreSeparatedByClass()
2308+
{
2309+
$this->assertFalse(EloquentModelBootingCallbackTestStub::$bootHasFinished);
2310+
2311+
$model = new EloquentModelBootingCallbackTestStub();
2312+
2313+
$this->assertTrue($model::$bootHasFinished);
2314+
2315+
$this->assertFalse(EloquentChildModelBootingCallbackTestStub::$bootHasFinished);
2316+
2317+
$model = new EloquentChildModelBootingCallbackTestStub();
2318+
2319+
$this->assertTrue($model::$bootHasFinished);
2320+
2321+
EloquentModelBootingCallbackTestStub::unboot();
2322+
EloquentChildModelBootingCallbackTestStub::unboot();
2323+
}
2324+
22962325
public function testModelsTraitIsInitialized()
22972326
{
22982327
$model = new EloquentModelStubWithTrait;
@@ -3602,6 +3631,7 @@ class EloquentModelBootingTestStub extends Model
36023631
public static function unboot()
36033632
{
36043633
unset(static::$booted[static::class]);
3634+
unset(static::$bootedCallbacks[static::class]);
36053635
}
36063636

36073637
public static function isBooted()
@@ -4121,3 +4151,30 @@ class EloquentModelWithUseFactoryAttribute extends Model
41214151
{
41224152
use HasFactory;
41234153
}
4154+
4155+
trait EloquentTraitBootingCallbackTestStub
4156+
{
4157+
public static function bootEloquentTraitBootingCallbackTestStub()
4158+
{
4159+
static::whenBooted(fn () => static::$bootHasFinished = true);
4160+
}
4161+
}
4162+
4163+
class EloquentModelBootingCallbackTestStub extends Model
4164+
{
4165+
use EloquentTraitBootingCallbackTestStub;
4166+
4167+
public static bool $bootHasFinished = false;
4168+
4169+
public static function unboot()
4170+
{
4171+
unset(static::$booted[static::class]);
4172+
unset(static::$bootedCallbacks[static::class]);
4173+
static::$bootHasFinished = false;
4174+
}
4175+
}
4176+
4177+
class EloquentChildModelBootingCallbackTestStub extends EloquentModelBootingCallbackTestStub
4178+
{
4179+
public static bool $bootHasFinished = false;
4180+
}

tests/Integration/Database/EloquentModelCustomEventsTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Tests\Integration\Database\EloquentModelCustomEventsTest;
44

5+
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
56
use Illuminate\Database\Eloquent\Model;
67
use Illuminate\Database\Schema\Blueprint;
78
use Illuminate\Support\Facades\Event;
@@ -24,6 +25,11 @@ protected function afterRefreshingDatabase()
2425
Schema::create('test_model1', function (Blueprint $table) {
2526
$table->increments('id');
2627
});
28+
29+
Schema::create('eloquent_model_stub_with_custom_event_from_traits', function (Blueprint $table) {
30+
$table->boolean('custom_attribute');
31+
$table->boolean('observer_attribute');
32+
});
2733
}
2834

2935
public function testFlushListenersClearsCustomEvents()
@@ -45,6 +51,19 @@ public function testCustomEventListenersAreFired()
4551

4652
$this->assertTrue($_SERVER['fired_event']);
4753
}
54+
55+
public function testAddObservableEventFromTrait()
56+
{
57+
$model = new EloquentModelStubWithCustomEventFromTrait();
58+
59+
$this->assertNull($model->custom_attribute);
60+
$this->assertNull($model->observer_attribute);
61+
62+
$model->completeCustomAction();
63+
64+
$this->assertTrue($model->custom_attribute);
65+
$this->assertTrue($model->observer_attribute);
66+
}
4867
}
4968

5069
class TestModel1 extends Model
@@ -59,3 +78,36 @@ class CustomEvent
5978
{
6079
//
6180
}
81+
82+
trait CustomEventTrait
83+
{
84+
public function completeCustomAction()
85+
{
86+
$this->custom_attribute = true;
87+
88+
$this->fireModelEvent('customEvent');
89+
}
90+
91+
public function initializeCustomEventTrait()
92+
{
93+
$this->addObservableEvents([
94+
'customEvent',
95+
]);
96+
}
97+
}
98+
99+
class CustomObserver
100+
{
101+
public function customEvent(EloquentModelStubWithCustomEventFromTrait $model)
102+
{
103+
$model->observer_attribute = true;
104+
}
105+
}
106+
107+
#[ObservedBy(CustomObserver::class)]
108+
class EloquentModelStubWithCustomEventFromTrait extends Model
109+
{
110+
use CustomEventTrait;
111+
112+
public $timestamps = false;
113+
}

0 commit comments

Comments
 (0)