From 8a0d0bd419443e2bfc75fc890692bf126035d5b4 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Thu, 8 May 2025 17:04:41 -0400 Subject: [PATCH 1/6] Add failing test --- tests/Database/DatabaseEloquentModelTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index d7f6f1538b72..ef1f9c4de7cf 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3330,6 +3330,21 @@ public function testUseFactoryAttribute() $this->assertEquals(EloquentModelWithUseFactoryAttribute::class, $factory->modelName()); $this->assertEquals('test name', $instance->name); // Small smoke test to ensure the factory is working } + + public function testNestedModelBootingIsDisallowed() + { + $this->expectExceptionMessageMatches('/".+" cannot be called on the ".+" class while it is already being booted\./'); + + $model = new class extends Model + { + protected static function boot() + { + parent::boot(); + + $tableName = (new static())->getTable(); + } + }; + } } class EloquentTestObserverStub From edcfa607dc7318a7e9a5b70e7a1e33288d181344 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Fri, 31 Jan 2025 16:06:58 -0500 Subject: [PATCH 2/6] Only mark a model class as booted after the boot process has been completed --- src/Illuminate/Database/Eloquent/Model.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index be5a2b3f1dbe..099f297ca7c1 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -286,12 +286,14 @@ public function __construct(array $attributes = []) protected function bootIfNotBooted() { if (! isset(static::$booted[static::class])) { - static::$booted[static::class] = true; $this->fireModelEvent('booting', false); static::booting(); static::boot(); + + static::$booted[static::class] = true; + static::booted(); static::$bootedCallbacks[static::class] ??= []; From b525a36f72a24c6d43f514a3848a798fbe0d0247 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Fri, 31 Jan 2025 16:08:03 -0500 Subject: [PATCH 3/6] Mark a model class as booting as soon as the boot process starts - A model class is removed from the `booting` array as soon as the boot process has completed. --- src/Illuminate/Database/Eloquent/Model.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 099f297ca7c1..844ff7af849c 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -157,6 +157,11 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static $bootedCallbacks = []; + /** + * Models currently being booted. + */ + protected static array $booting = []; + /** * The array of trait initializers that will be called on each new instance. * @@ -287,12 +292,15 @@ protected function bootIfNotBooted() { if (! isset(static::$booted[static::class])) { + static::$booting[static::class] = true; + $this->fireModelEvent('booting', false); static::booting(); static::boot(); static::$booted[static::class] = true; + unset(static::$booting[static::class]); static::booted(); From ad7a45dcdee1a9c8070429ae7e9fa553bd9fa134 Mon Sep 17 00:00:00 2001 From: Will Rowe Date: Thu, 8 May 2025 16:39:17 -0400 Subject: [PATCH 4/6] Throw an exception if `bootIfNotBooted` is called while booting - This infers that something in the boot process expected the boot to already be finished. - If we just ignore the call then the model class would be in an inconsistent state for whichever code called it. --- src/Illuminate/Database/Eloquent/Model.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 844ff7af849c..9a4e8da7ff43 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -4,6 +4,7 @@ use ArrayAccess; use Closure; +use Exception; use Illuminate\Contracts\Broadcasting\HasBroadcastChannel; use Illuminate\Contracts\Queue\QueueableCollection; use Illuminate\Contracts\Queue\QueueableEntity; @@ -291,6 +292,9 @@ public function __construct(array $attributes = []) protected function bootIfNotBooted() { if (! isset(static::$booted[static::class])) { + if (isset(static::$booting[static::class])) { + throw new Exception('"'.__METHOD__.'" cannot be called on the "'.static::class.'" class while it is already being booted.'); + } static::$booting[static::class] = true; From bdc81f682e68648651f1cf73f94c4562beb7f67d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 10 May 2025 13:16:31 -0500 Subject: [PATCH 5/6] formatting --- src/Illuminate/Database/Eloquent/Model.php | 2 +- tests/Database/DatabaseEloquentModelTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 9a4e8da7ff43..9a22fd937c1c 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -293,7 +293,7 @@ protected function bootIfNotBooted() { if (! isset(static::$booted[static::class])) { if (isset(static::$booting[static::class])) { - throw new Exception('"'.__METHOD__.'" cannot be called on the "'.static::class.'" class while it is already being booted.'); + throw new LogicException('The ['.__METHOD__.'] method may not be called on model ['.static::class.'] while it is being booted.'); } static::$booting[static::class] = true; diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index ef1f9c4de7cf..54c2673b3b6f 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3333,7 +3333,7 @@ public function testUseFactoryAttribute() public function testNestedModelBootingIsDisallowed() { - $this->expectExceptionMessageMatches('/".+" cannot be called on the ".+" class while it is already being booted\./'); + $this->expectExceptionMessageMatches('/The \[(.+)] method may not be called on model \[(.+)\] while it is being booted\./'); $model = new class extends Model { From 6989e84c78676cec78c7d1284a6fb896658e484a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 10 May 2025 13:17:26 -0500 Subject: [PATCH 6/6] formatting --- src/Illuminate/Database/Eloquent/Model.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 9a22fd937c1c..adcf93843d5b 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -144,6 +144,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static $dispatcher; + /** + * The models that are currently being booted. + * + * @var array + */ + protected static $booting = []; + /** * The array of booted models. * @@ -158,11 +165,6 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt */ protected static $bootedCallbacks = []; - /** - * Models currently being booted. - */ - protected static array $booting = []; - /** * The array of trait initializers that will be called on each new instance. *