Skip to content
52 changes: 50 additions & 2 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,26 @@ 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 = [];

/**
* The inherited global scopes.
*
* @var array
*/
protected $inheritedScopes = [];

/**
* The callbacks that should be invoked after retrieving data from the database.
*
Expand Down Expand Up @@ -204,6 +211,21 @@ public function withGlobalScope($identifier, $scope)
return $this;
}

/**
* Register many new global scopes.
*
* @param array $scopes
* @return $this
*/
public function withGlobalScopes(array $scopes)
{
foreach ($scopes as $identifier => $scope) {
$this->withGlobalScope($identifier, $scope);
}

return $this;
}

/**
* Remove a registered global scope.
*
Expand Down Expand Up @@ -257,6 +279,25 @@ public function withoutGlobalScopesExcept(array $scopes = [])
return $this;
}

/**
* Register scopes that related model eager loading queries 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.
*
Expand Down Expand Up @@ -940,6 +981,13 @@ protected function eagerLoadRelation(array $models, $name, Closure $constraints)

$relation->addEagerConstraints($models);

if (! empty($this->inheritedScopes)) {
$relation
->withGlobalScopes($this->inheritedScopes['keep'] ?? [])
->withoutGlobalScopes($this->inheritedScopes['remove'] ?? [])
->inheritScopes(...$this->inheritedScopes);
}

$constraints($relation);

// Once we have the results, we just match those back up to their parent models
Expand Down
15 changes: 14 additions & 1 deletion src/Illuminate/Database/Eloquent/SoftDeletingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(remove: [$this]);
});
}

/**
* Add the without-trashed extension to the builder.
*
Expand Down
31 changes: 31 additions & 0 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,37 @@ 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;
}]);
$scope = function ($query) {
$query->where('active', 1);
};
$builder->withGlobalScope('test_scope', $scope);
$builder->inheritScopes(['test_scope' => $scope]);

$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();
Expand Down
20 changes: 20 additions & 0 deletions tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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...
*
Expand Down
18 changes: 18 additions & 0 deletions tests/Database/DatabaseSoftDeletingScopeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()->with([], [$scope])->andReturn($givenBuilder);
$result = $callback($givenBuilder);

$this->assertEquals($givenBuilder, $result);
}

public function testOnlyTrashedExtension()
{
$builder = new EloquentBuilder(new BaseBuilder(
Expand Down
Loading