Skip to content

Commit

Permalink
[5.2] Refactor relations and scopes (#13824)
Browse files Browse the repository at this point in the history
* Refactor relations and scopes

* more refactoring

* styleCI
  • Loading branch information
acasar authored and taylorotwell committed Jun 1, 2016
1 parent 35b4360 commit c9d9748
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 141 deletions.
100 changes: 29 additions & 71 deletions src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ public function getRelation($name)
* @param string $relation
* @return array
*/
protected function nestedRelations($relation)
public function nestedRelations($relation)
{
$nested = [];

Expand Down Expand Up @@ -822,7 +822,7 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', C
$query = $relation->{$queryType}($relation->getRelated()->newQuery(), $this);

if ($callback) {
$this->applyCallbackToQuery($callback, [$query], $query->getQuery());
$query->callScope($callback);
}

return $this->addHasWhere(
Expand Down Expand Up @@ -936,7 +936,7 @@ public function orWhereHas($relation, Closure $callback, $operator = '>=', $coun
*/
protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
{
$this->mergeModelDefinedRelationWheresToHasQuery($hasQuery, $relation);
$hasQuery->mergeModelDefinedRelationConstraints($relation->getQuery());

if ($this->shouldRunExistsQuery($operator, $count)) {
$not = ($operator === '<' && $count === 1);
Expand Down Expand Up @@ -980,22 +980,21 @@ protected function whereCountQuery(QueryBuilder $query, $operator = '>=', $count
}

/**
* Merge the "wheres" from a relation query to a has query.
* Merge the constraints from a relation query to the current query.
*
* @param \Illuminate\Database\Eloquent\Builder $hasQuery
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @param \Illuminate\Database\Eloquent\Builder $relation
* @return void
*/
protected function mergeModelDefinedRelationWheresToHasQuery(Builder $hasQuery, Relation $relation)
public function mergeModelDefinedRelationConstraints(Builder $relation)
{
$removedScopes = $hasQuery->removedScopes();
$removedScopes = $relation->removedScopes();

$relationQuery = $relation->withoutGlobalScopes($removedScopes)->toBase();
$relationQuery = $relation->getQuery();

// Here we have the "has" query and the original relation. We need to copy over any
// where clauses the developer may have put in the relationship function over to
// the has query, and then copy the bindings from the "has" query to the main.
$hasQuery->withoutGlobalScopes()->mergeWheres(
// Here we have some relation query and the original relation. We need to copy over any
// where clauses that the developer may have put in the relation definition function.
// We need to remove any global scopes that the developer already removed as well.
return $this->withoutGlobalScopes($removedScopes)->mergeWheres(
$relationQuery->wheres, $relationQuery->getBindings()
);
}
Expand Down Expand Up @@ -1056,9 +1055,9 @@ public function withCount($relations)
$relation->getRelated()->newQuery(), $this
);

call_user_func($constraints, $query);
$query->callScope($constraints);

$this->mergeModelDefinedRelationWheresToHasQuery($query, $relation);
$query->mergeModelDefinedRelationConstraints($relation->getQuery());

$this->selectSub($query->toBase(), snake_case($name).'_count');
}
Expand Down Expand Up @@ -1127,37 +1126,24 @@ protected function parseNestedWith($name, $results)
}

/**
* Call the given model scope on the underlying model.
*
* @param string $scope
* @param array $parameters
* @return \Illuminate\Database\Query\Builder
*/
protected function callScope($scope, $parameters)
{
array_unshift($parameters, $this);

return $this->applyCallbackToQuery([$this->model, $scope], $parameters);
}

/**
* Apply the given callback to a supplied (or the current) builder instance.
* Apply the given scope on the current builder instance.
*
* @param callable $callback
* @param callable $scope
* @param array $parameters
* @param \Illuminate\Database\Query\Builder $query
* @return mixed
*/
protected function applyCallbackToQuery(callable $callback, $parameters = [], $query = null)
protected function callScope(callable $scope, $parameters = [])
{
$query = $query ?: $this->getQuery();
array_unshift($parameters, $this);

$query = $this->getQuery();

// We will keep track of how many wheres are on the query before running the
// scope so that we can properly group the added scope constraints in the
// query as their own isolated nested where statement and avoid issues.
$originalWhereCount = count($query->wheres);

$result = call_user_func_array($callback, $parameters) ?: $this;
$result = call_user_func_array($scope, $parameters) ?: $this;

if ($this->shouldNestWheresForScope($query, $originalWhereCount)) {
$this->nestWheresForScope($query, $originalWhereCount);
Expand All @@ -1179,47 +1165,19 @@ public function applyScopes()

$builder = clone $this;

$query = $builder->getQuery();

// We will keep track of how many wheres are on the query before running the
// scope so that we can properly group the added scope constraints in the
// query as their own isolated nested where statement and avoid issues.
$originalWhereCount = count($query->wheres);

$whereCounts = [$originalWhereCount];

foreach ($this->scopes as $scope) {
$this->applyScope($scope, $builder);

// Again, we will keep track of the count each time we add where clauses so that
// we will properly isolate each set of scope constraints inside of their own
// nested where clause to avoid any conflicts or issues with logical order.
$whereCounts[] = count($query->wheres);
}

if ($this->shouldNestWheresForScope($query, $originalWhereCount)) {
$this->nestWheresForScope($query, $whereCounts);
$builder->callScope(function (Builder $builder) use ($scope) {
if ($scope instanceof Closure) {
$scope($builder);
} elseif ($scope instanceof Scope) {
$scope->apply($builder, $this->getModel());
}
});
}

return $builder;
}

/**
* Apply a single scope on the given builder instance.
*
* @param \Illuminate\Database\Eloquent\Scope|\Closure $scope
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
protected function applyScope($scope, $builder)
{
if ($scope instanceof Closure) {
$scope($builder);
} elseif ($scope instanceof Scope) {
$scope->apply($builder, $this->getModel());
}
}

/**
* Determine if the scope added after the given offset should be nested.
*
Expand Down Expand Up @@ -1424,7 +1382,7 @@ public function __call($method, $parameters)
}

if (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
return $this->callScope($scope, $parameters);
return $this->callScope([$this->model, $scope], $parameters);
}

if (in_array($method, $this->passthru)) {
Expand Down
10 changes: 3 additions & 7 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,15 +381,11 @@ public static function hasGlobalScope($scope)
*/
public static function getGlobalScope($scope)
{
$modelScopes = Arr::get(static::$globalScopes, static::class, []);

if (is_string($scope)) {
return isset($modelScopes[$scope]) ? $modelScopes[$scope] : null;
if (! is_string($scope)) {
$scope = get_class($scope);
}

return Arr::first($modelScopes, function ($key, $value) use ($scope) {
return $scope instanceof $value;
});
return Arr::get(static::$globalScopes, static::class.'.'.$scope);
}

/**
Expand Down
42 changes: 3 additions & 39 deletions src/Illuminate/Database/Eloquent/Relations/MorphTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

namespace Illuminate\Database\Eloquent\Relations;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;

class MorphTo extends BelongsTo
{
Expand Down Expand Up @@ -177,48 +175,14 @@ protected function getResultsByType($type)

$key = $instance->getTable().'.'.$instance->getKeyName();

$query = $instance->newQuery();
$eagerLoads = $this->getQuery()->nestedRelations($this->relation);

$query->setEagerLoads($this->getEagerLoadsForInstance($instance));

$this->mergeRelationWheresToMorphQuery($this->query, $query);
$query = $instance->newQuery()->setEagerLoads($eagerLoads)
->mergeModelDefinedRelationConstraints($this->getQuery());

return $query->whereIn($key, $this->gatherKeysByType($type)->all())->get();
}

/**
* Get the relationships that should be eager loaded for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $instance
* @return array
*/
protected function getEagerLoadsForInstance(Model $instance)
{
$relations = BaseCollection::make($this->query->getEagerLoads());

return $relations->filter(function ($constraint, $relation) {
return Str::startsWith($relation, $this->relation.'.');
})->keyBy(function ($constraint, $relation) {
return Str::replaceFirst($this->relation.'.', '', $relation);
})->merge($instance->getEagerLoads())->all();
}

/**
* Merge the "wheres" from a relation query to a morph query.
*
* @param \Illuminate\Database\Eloquent\Builder $relationQuery
* @param \Illuminate\Database\Eloquent\Builder $morphQuery
* @return void
*/
protected function mergeRelationWheresToMorphQuery(Builder $relationQuery, Builder $morphQuery)
{
$removedScopes = $relationQuery->removedScopes();

$morphQuery->withoutGlobalScopes($removedScopes)->mergeWheres(
$relationQuery->getQuery()->wheres, $relationQuery->getBindings()
);
}

/**
* Gather all of the foreign keys for a given type.
*
Expand Down
24 changes: 0 additions & 24 deletions src/Illuminate/Database/Eloquent/SoftDeletes.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,30 +103,6 @@ public function trashed()
return ! is_null($this->{$this->getDeletedAtColumn()});
}

/**
* Get a new query builder that includes soft deletes.
*
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public static function withTrashed()
{
return (new static)->newQueryWithoutScope(new SoftDeletingScope);
}

/**
* Get a new query builder that only includes soft deletes.
*
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public static function onlyTrashed()
{
$instance = new static;

$column = $instance->getQualifiedDeletedAtColumn();

return $instance->newQueryWithoutScope(new SoftDeletingScope)->whereNotNull($column);
}

/**
* Register a restoring model event with the dispatcher.
*
Expand Down

0 comments on commit c9d9748

Please sign in to comment.