From a912d2acb358810f0592574f75d4bfb10d649954 Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 00:31:03 +0000 Subject: [PATCH 01/12] Add a way to set multiple global scopes --- src/Illuminate/Database/Eloquent/Builder.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index d32a17b207ef..3be757d161a8 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -204,6 +204,22 @@ public function withGlobalScope($identifier, $scope) return $this; } + /** + * Register many new global scopes. + * + * @param array $scopes + * @param \Illuminate\Database\Eloquent\Scope|\Closure $scope + * @return $this + */ + public function withGlobalScopes(array $scopes) + { + foreach ($scopes as $identifier => $scope) { + $this->withGlobalScope($identifier, $scope); + } + + return $this; + } + /** * Remove a registered global scope. * From 5861d881c0d5f19fd400ee2b070875f1fb9354b3 Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 00:33:07 +0000 Subject: [PATCH 02/12] Allow related models to inherit scopes --- src/Illuminate/Database/Eloquent/Builder.php | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 3be757d161a8..168a034c71c3 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -137,6 +137,13 @@ class Builder implements BuilderContract 'torawsql', ]; + /** + * Indicate if the related models should inherit the scopes from the parent. + * + * @var bool + */ + protected $inheritScopes = false; + /** * Applied global scopes. * @@ -956,6 +963,14 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); + // Run this before the callback so if the user wanted to they can overwrite it. + if ($this->inheritScopes) { + $relation + ->withGlobalScopes($this->scopes) + ->withoutGlobalScopes($this->removedScopes) + ->inheritScopes(); + } + $constraints($relation); // Once we have the results, we just match those back up to their parent models @@ -1517,6 +1532,18 @@ public function onDelete(Closure $callback) $this->onDelete = $callback; } + /** + * Apply current scopes to all relationships. + * + * @return $this + */ + public function inheritScopes() + { + $this->inheritScopes = true; + + return $this; + } + /** * Determine if the given model has a scope. * From 9bdded33146c47dbe83f95e737de5a3824f549fb Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 00:37:00 +0000 Subject: [PATCH 03/12] Add `withTrashedRelations()` --- .../Database/Eloquent/SoftDeletingScope.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index d1ef0d22b9b9..140abf230935 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -9,7 +9,7 @@ class SoftDeletingScope implements Scope * * @var string[] */ - protected $extensions = ['Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed']; + protected $extensions = ['Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithTrashedRelations', 'WithoutTrashed', 'OnlyTrashed']; /** * Apply the scope to a given Eloquent query builder. @@ -127,6 +127,19 @@ protected function addWithTrashed(Builder $builder) }); } + /** + * Add the with-trashed relationships extension to the builder. + * + * @param \Illuminate\Database\Eloquent\Builder<*> $builder + * @return void + */ + protected function addWithTrashedRelations(Builder $builder) + { + $builder->macro('withTrashedRelations', function (Builder $builder) { + return $builder->withTrashed()->inheritScopes(); + }); + } + /** * Add the without-trashed extension to the builder. * From 92c8cbdff631a3cce837b300f7d8c82d0e70f936 Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 00:39:47 +0000 Subject: [PATCH 04/12] Formatting --- src/Illuminate/Database/Eloquent/Builder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 168a034c71c3..8386e55e4f6f 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -215,7 +215,6 @@ public function withGlobalScope($identifier, $scope) * Register many new global scopes. * * @param array $scopes - * @param \Illuminate\Database\Eloquent\Scope|\Closure $scope * @return $this */ public function withGlobalScopes(array $scopes) From 6a7f10700a380a3101444550129d12feeab806fa Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 15:51:17 +0000 Subject: [PATCH 05/12] Add test for macro --- .../Database/DatabaseSoftDeletingScopeTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Database/DatabaseSoftDeletingScopeTest.php b/tests/Database/DatabaseSoftDeletingScopeTest.php index adf03ac6dc3c..5e06c08dbf0b 100644 --- a/tests/Database/DatabaseSoftDeletingScopeTest.php +++ b/tests/Database/DatabaseSoftDeletingScopeTest.php @@ -112,6 +112,24 @@ public function testWithTrashedExtension() $this->assertEquals($givenBuilder, $result); } + public function testWithTrashedRelationsExtension() + { + $builder = new EloquentBuilder(new BaseBuilder( + m::mock(ConnectionInterface::class), + m::mock(Grammar::class), + m::mock(Processor::class) + )); + $scope = m::mock(SoftDeletingScope::class.'[remove]'); + $scope->extend($builder); + $callback = $builder->getMacro('withTrashedRelations'); + $givenBuilder = m::mock(EloquentBuilder::class); + $givenBuilder->shouldReceive('withTrashed')->once()->andReturn($givenBuilder); + $givenBuilder->shouldReceive('inheritScopes')->once()->andReturn($givenBuilder); + $result = $callback($givenBuilder); + + $this->assertEquals($givenBuilder, $result); + } + public function testOnlyTrashedExtension() { $builder = new EloquentBuilder(new BaseBuilder( From 6628009e250b249408307252cb10c4e34dcedde4 Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 16:17:42 +0000 Subject: [PATCH 06/12] Add mock test for scoped relationships --- .../Database/DatabaseEloquentBuilderTest.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index c031a6c80099..807439f7d359 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -858,6 +858,36 @@ public function testRelationshipEagerLoadProcess() unset($_SERVER['__eloquent.constrain']); } + public function testScopedRelationshipEagerLoadProcess() + { + $builder = m::mock(Builder::class.'[getRelation]', [$this->getMockQueryBuilder()]); + $builder->setEagerLoads(['orders' => function ($query) { + $_SERVER['__eloquent.constrain'] = $query; + }]); + $builder->withGlobalScope('test_scope', function ($query) { + $query->where('active', 1); + }); + $builder->inheritScopes(); + + $relation = m::mock(stdClass::class); + $relation->shouldReceive('addEagerConstraints')->once()->with(['models']); + $relation->shouldReceive('withGlobalScopes')->once()->with(m::on(function ($scopes) { + return isset($scopes['test_scope']); + }))->andReturnSelf(); + $relation->shouldReceive('withoutGlobalScopes')->once()->with(m::type('array'))->andReturnSelf(); + $relation->shouldReceive('inheritScopes')->once()->andReturnSelf(); + $relation->shouldReceive('initRelation')->once()->with(['models'], 'orders')->andReturn(['models']); + $relation->shouldReceive('getEager')->once()->andReturn(['results']); + $relation->shouldReceive('match')->once()->with(['models'], ['results'], 'orders')->andReturn(['models.matched']); + $builder->shouldReceive('getRelation')->once()->with('orders')->andReturn($relation); + + $results = $builder->eagerLoadRelations(['models']); + + $this->assertEquals(['models.matched'], $results); + $this->assertEquals($relation, $_SERVER['__eloquent.constrain']); + unset($_SERVER['__eloquent.constrain']); + } + public function testRelationshipEagerLoadProcessForImplicitlyEmpty() { $queryBuilder = $this->getMockQueryBuilder(); From 45a515edc4494ab46a2ff7ce20650acdcd335dbf Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Thu, 6 Nov 2025 16:47:10 +0000 Subject: [PATCH 07/12] Add integration test --- ...baseEloquentSoftDeletesIntegrationTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php index 195e2dfd7e17..eebf707c9f3a 100644 --- a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php +++ b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php @@ -967,6 +967,26 @@ public function testSelfReferencingRelationshipWithSoftDeletes() $this->assertEquals(1, SoftDeletesTestUser::whereHas('self_referencing')->count()); } + public function testAllRelationshipsLoadWithSoftDeletes() + { + [$taylor, $abigail] = $this->createUsers(); + + $post1 = tap($taylor->posts()->create(['title' => 'First Title']))->delete(); + + $post2 = $abigail->posts()->create(['title' => 'First Title']); + $post3 = tap($abigail->posts()->create(['title' => 'Second Title']))->delete(); + + tap($post1->comments()->create(['body' => 'Comment 1']))->delete(); + $post2->comments()->create(['body' => 'Comment 2']); + tap($post2->comments()->create(['body' => 'Comment 3']))->delete(); + $post3->comments()->create(['body' => 'Comment 4']); + + $users = SoftDeletesTestUser::with('posts.comments')->withTrashedRelations()->get(); + $this->assertCount(2, $users); + $this->assertCount(3, $users->pluck('posts')->collapse()); + $this->assertCount(4, $users->pluck('posts')->collapse()->pluck('comments')->collapse()); + } + /** * Helpers... * From 219bb2d5852df4d5f014c3bb234d02984f7aae70 Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Fri, 7 Nov 2025 12:33:28 +0000 Subject: [PATCH 08/12] Support choosing scopes --- src/Illuminate/Database/Eloquent/Builder.php | 55 +++++++++++-------- .../Database/Eloquent/SoftDeletingScope.php | 2 +- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 8386e55e4f6f..6a8b200eba21 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -137,13 +137,6 @@ class Builder implements BuilderContract 'torawsql', ]; - /** - * Indicate if the related models should inherit the scopes from the parent. - * - * @var bool - */ - protected $inheritScopes = false; - /** * Applied global scopes. * @@ -158,6 +151,13 @@ class Builder implements BuilderContract */ protected $removedScopes = []; + /** + * Inherited global scopes. + * + * @var array + */ + protected $inheritedScopes = []; + /** * The callbacks that should be invoked after retrieving data from the database. * @@ -279,6 +279,25 @@ public function withoutGlobalScopesExcept(array $scopes = []) return $this; } + /** + * Register scopes that related models should inherit. + * + * @param array $scopes + * @return $this + */ + public function inheritScopes(array $keep = [], array $remove = []) + { + foreach ($keep as $identifier => $scope) { + $this->inheritedScopes['keep'][$identifier] = $scope; + } + + foreach ($remove as $identifier => $scope) { + $this->inheritedScopes['remove'][$identifier] = $scope; + } + + return $this; + } + /** * Get an array of global scopes that were removed from the query. * @@ -962,12 +981,12 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); - // Run this before the callback so if the user wanted to they can overwrite it. - if ($this->inheritScopes) { + // Run this before the callback so if the user wanted to they can override it. + if (!empty($this->inheritedScopes)) { $relation - ->withGlobalScopes($this->scopes) - ->withoutGlobalScopes($this->removedScopes) - ->inheritScopes(); + ->withGlobalScopes($this->inheritedScopes['keep'] ?? []) + ->withoutGlobalScopes($this->inheritedScopes['remove'] ?? []) + ->inheritScopes(...$this->inheritedScopes); } $constraints($relation); @@ -1531,18 +1550,6 @@ public function onDelete(Closure $callback) $this->onDelete = $callback; } - /** - * Apply current scopes to all relationships. - * - * @return $this - */ - public function inheritScopes() - { - $this->inheritScopes = true; - - return $this; - } - /** * Determine if the given model has a scope. * diff --git a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php index 140abf230935..3d5877484f66 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletingScope.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletingScope.php @@ -136,7 +136,7 @@ protected function addWithTrashed(Builder $builder) protected function addWithTrashedRelations(Builder $builder) { $builder->macro('withTrashedRelations', function (Builder $builder) { - return $builder->withTrashed()->inheritScopes(); + return $builder->withTrashed()->inheritScopes(remove: [$this]); }); } From f11dd9b9402a3abcb9f51328e434c42f499ddf5f Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Fri, 7 Nov 2025 13:27:34 +0000 Subject: [PATCH 09/12] Update tests --- tests/Database/DatabaseEloquentBuilderTest.php | 7 ++++--- tests/Database/DatabaseSoftDeletingScopeTest.php | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 807439f7d359..5938b1c2ca34 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -864,10 +864,11 @@ public function testScopedRelationshipEagerLoadProcess() $builder->setEagerLoads(['orders' => function ($query) { $_SERVER['__eloquent.constrain'] = $query; }]); - $builder->withGlobalScope('test_scope', function ($query) { + $scope = function ($query) { $query->where('active', 1); - }); - $builder->inheritScopes(); + }; + $builder->withGlobalScope('test_scope', $scope); + $builder->inheritScopes(['test_scope' => $scope]); $relation = m::mock(stdClass::class); $relation->shouldReceive('addEagerConstraints')->once()->with(['models']); diff --git a/tests/Database/DatabaseSoftDeletingScopeTest.php b/tests/Database/DatabaseSoftDeletingScopeTest.php index 5e06c08dbf0b..f4871c938931 100644 --- a/tests/Database/DatabaseSoftDeletingScopeTest.php +++ b/tests/Database/DatabaseSoftDeletingScopeTest.php @@ -124,7 +124,7 @@ public function testWithTrashedRelationsExtension() $callback = $builder->getMacro('withTrashedRelations'); $givenBuilder = m::mock(EloquentBuilder::class); $givenBuilder->shouldReceive('withTrashed')->once()->andReturn($givenBuilder); - $givenBuilder->shouldReceive('inheritScopes')->once()->andReturn($givenBuilder); + $givenBuilder->shouldReceive('inheritScopes')->once()->with([], [$scope])->andReturn($givenBuilder); $result = $callback($givenBuilder); $this->assertEquals($givenBuilder, $result); From 854d8ee128a638f7629448d3cab189d9ab4648f3 Mon Sep 17 00:00:00 2001 From: Alex Wass Date: Fri, 7 Nov 2025 13:28:27 +0000 Subject: [PATCH 10/12] Formatting --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 6a8b200eba21..3bd2a8e01bb8 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -982,7 +982,7 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); // Run this before the callback so if the user wanted to they can override it. - if (!empty($this->inheritedScopes)) { + if (! empty($this->inheritedScopes)) { $relation ->withGlobalScopes($this->inheritedScopes['keep'] ?? []) ->withoutGlobalScopes($this->inheritedScopes['remove'] ?? []) From 75507ca1edd688eb1dec3196ccd43414e22b9dfb Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 7 Nov 2025 14:40:27 -0600 Subject: [PATCH 11/12] formatting --- src/Illuminate/Database/Eloquent/Builder.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 3bd2a8e01bb8..dc59243c5499 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -138,21 +138,21 @@ class Builder implements BuilderContract ]; /** - * Applied global scopes. + * The applied global scopes. * * @var array */ protected $scopes = []; /** - * Removed global scopes. + * The removed global scopes. * * @var array */ protected $removedScopes = []; /** - * Inherited global scopes. + * The inherited global scopes. * * @var array */ @@ -280,7 +280,7 @@ public function withoutGlobalScopesExcept(array $scopes = []) } /** - * Register scopes that related models should inherit. + * Register scopes that related model queries should inherit. * * @param array $scopes * @return $this @@ -981,7 +981,6 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints) $relation->addEagerConstraints($models); - // Run this before the callback so if the user wanted to they can override it. if (! empty($this->inheritedScopes)) { $relation ->withGlobalScopes($this->inheritedScopes['keep'] ?? []) From 779241b7a9465cf34baf71c3e7c5840428152a23 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 7 Nov 2025 14:40:44 -0600 Subject: [PATCH 12/12] formatting --- src/Illuminate/Database/Eloquent/Builder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index dc59243c5499..0199361721f0 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -280,7 +280,7 @@ public function withoutGlobalScopesExcept(array $scopes = []) } /** - * Register scopes that related model queries should inherit. + * Register scopes that related model eager loading queries should inherit. * * @param array $scopes * @return $this