Skip to content

[12.x] Fix support for adding custom observable events from traits #55286

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
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
2 changes: 1 addition & 1 deletion src/Illuminate/Database/Eloquent/Concerns/HasEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ trait HasEvents
*/
public static function bootHasEvents()
{
static::observe(static::resolveObserveAttributes());
static::whenBooted(fn () => static::observe(static::resolveObserveAttributes()));
}

/**
Expand Down
28 changes: 28 additions & 0 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Database\Eloquent;

use ArrayAccess;
use Closure;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
Expand Down Expand Up @@ -150,6 +151,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
*/
protected static $booted = [];

/**
* The callbacks that should be executed after the model has booted.
*
* @var array
*/
protected static $bootedCallbacks = [];

/**
* The array of trait initializers that will be called on each new instance.
*
Expand Down Expand Up @@ -280,6 +288,12 @@ protected function bootIfNotBooted()
static::boot();
static::booted();

static::$bootedCallbacks[static::class] ??= [];

foreach (static::$bootedCallbacks[static::class] as $callback) {
$callback();
}

$this->fireModelEvent('booted', false);
}
}
Expand Down Expand Up @@ -358,6 +372,19 @@ protected static function booted()
//
}

/**
* Register a closure to be executed after the model has booted.
*
* @param \Closure $callback
* @return void
*/
protected static function whenBooted(Closure $callback)
{
static::$bootedCallbacks[static::class] ??= [];

static::$bootedCallbacks[static::class][] = $callback;
}

/**
* Clear the list of booted models so they will be re-booted.
*
Expand All @@ -366,6 +393,7 @@ protected static function booted()
public static function clearBootedModels()
{
static::$booted = [];
static::$bootedCallbacks = [];

static::$globalScopes = [];
}
Expand Down
57 changes: 57 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2293,6 +2293,35 @@ public function testModelIsBootedOnUnserialize()
$this->assertTrue(EloquentModelBootingTestStub::isBooted());
}

public function testCallbacksCanBeRunAfterBootingHasFinished()
{
$this->assertFalse(EloquentModelBootingCallbackTestStub::$bootHasFinished);

$model = new EloquentModelBootingCallbackTestStub();

$this->assertTrue($model::$bootHasFinished);

EloquentModelBootingCallbackTestStub::unboot();
}

public function testBootedCallbacksAreSeparatedByClass()
{
$this->assertFalse(EloquentModelBootingCallbackTestStub::$bootHasFinished);

$model = new EloquentModelBootingCallbackTestStub();

$this->assertTrue($model::$bootHasFinished);

$this->assertFalse(EloquentChildModelBootingCallbackTestStub::$bootHasFinished);

$model = new EloquentChildModelBootingCallbackTestStub();

$this->assertTrue($model::$bootHasFinished);

EloquentModelBootingCallbackTestStub::unboot();
EloquentChildModelBootingCallbackTestStub::unboot();
}

public function testModelsTraitIsInitialized()
{
$model = new EloquentModelStubWithTrait;
Expand Down Expand Up @@ -3602,6 +3631,7 @@ class EloquentModelBootingTestStub extends Model
public static function unboot()
{
unset(static::$booted[static::class]);
unset(static::$bootedCallbacks[static::class]);
}

public static function isBooted()
Expand Down Expand Up @@ -4121,3 +4151,30 @@ class EloquentModelWithUseFactoryAttribute extends Model
{
use HasFactory;
}

trait EloquentTraitBootingCallbackTestStub
{
public static function bootEloquentTraitBootingCallbackTestStub()
{
static::whenBooted(fn () => static::$bootHasFinished = true);
}
}

class EloquentModelBootingCallbackTestStub extends Model
{
use EloquentTraitBootingCallbackTestStub;

public static bool $bootHasFinished = false;

public static function unboot()
{
unset(static::$booted[static::class]);
unset(static::$bootedCallbacks[static::class]);
static::$bootHasFinished = false;
}
}

class EloquentChildModelBootingCallbackTestStub extends EloquentModelBootingCallbackTestStub
{
public static bool $bootHasFinished = false;
}
52 changes: 52 additions & 0 deletions tests/Integration/Database/EloquentModelCustomEventsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Tests\Integration\Database\EloquentModelCustomEventsTest;

use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Event;
Expand All @@ -24,6 +25,11 @@ protected function afterRefreshingDatabase()
Schema::create('test_model1', function (Blueprint $table) {
$table->increments('id');
});

Schema::create('eloquent_model_stub_with_custom_event_from_traits', function (Blueprint $table) {
$table->boolean('custom_attribute');
$table->boolean('observer_attribute');
});
}

public function testFlushListenersClearsCustomEvents()
Expand All @@ -45,6 +51,19 @@ public function testCustomEventListenersAreFired()

$this->assertTrue($_SERVER['fired_event']);
}

public function testAddObservableEventFromTrait()
{
$model = new EloquentModelStubWithCustomEventFromTrait();

$this->assertNull($model->custom_attribute);
$this->assertNull($model->observer_attribute);

$model->completeCustomAction();

$this->assertTrue($model->custom_attribute);
$this->assertTrue($model->observer_attribute);
}
}

class TestModel1 extends Model
Expand All @@ -59,3 +78,36 @@ class CustomEvent
{
//
}

trait CustomEventTrait
{
public function completeCustomAction()
{
$this->custom_attribute = true;

$this->fireModelEvent('customEvent');
}

public function initializeCustomEventTrait()
{
$this->addObservableEvents([
'customEvent',
]);
}
}

class CustomObserver
{
public function customEvent(EloquentModelStubWithCustomEventFromTrait $model)
{
$model->observer_attribute = true;
}
}

#[ObservedBy(CustomObserver::class)]
class EloquentModelStubWithCustomEventFromTrait extends Model
{
use CustomEventTrait;

public $timestamps = false;
}
Loading