123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- <?php
-
- namespace Illuminate\Database\Eloquent\Concerns;
-
- use Closure;
- use Illuminate\Support\Str;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Database\Query\Expression;
- use Illuminate\Database\Eloquent\Relations\Relation;
- use Illuminate\Database\Query\Builder as QueryBuilder;
-
- trait QueriesRelationships
- {
- /**
- * Add a relationship count / exists condition to the query.
- *
- * @param string $relation
- * @param string $operator
- * @param int $count
- * @param string $boolean
- * @param \Closure|null $callback
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
- {
- if (strpos($relation, '.') !== false) {
- return $this->hasNested($relation, $operator, $count, $boolean, $callback);
- }
-
- $relation = $this->getRelationWithoutConstraints($relation);
-
- // If we only need to check for the existence of the relation, then we can optimize
- // the subquery to only run a "where exists" clause instead of this full "count"
- // clause. This will make these queries run much faster compared with a count.
- $method = $this->canUseExistsForExistenceCheck($operator, $count)
- ? 'getRelationExistenceQuery'
- : 'getRelationExistenceCountQuery';
-
- $hasQuery = $relation->{$method}(
- $relation->getRelated()->newQuery(), $this
- );
-
- // Next we will call any given callback as an "anonymous" scope so they can get the
- // proper logical grouping of the where clauses if needed by this Eloquent query
- // builder. Then, we will be ready to finalize and return this query instance.
- if ($callback) {
- $hasQuery->callScope($callback);
- }
-
- return $this->addHasWhere(
- $hasQuery, $relation, $operator, $count, $boolean
- );
- }
-
- /**
- * Add nested relationship count / exists conditions to the query.
- *
- * Sets up recursive call to whereHas until we finish the nested relation.
- *
- * @param string $relations
- * @param string $operator
- * @param int $count
- * @param string $boolean
- * @param \Closure|null $callback
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- protected function hasNested($relations, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
- {
- $relations = explode('.', $relations);
-
- $closure = function ($q) use (&$closure, &$relations, $operator, $count, $callback) {
- // In order to nest "has", we need to add count relation constraints on the
- // callback Closure. We'll do this by simply passing the Closure its own
- // reference to itself so it calls itself recursively on each segment.
- count($relations) > 1
- ? $q->whereHas(array_shift($relations), $closure)
- : $q->has(array_shift($relations), $operator, $count, 'and', $callback);
- };
-
- return $this->has(array_shift($relations), '>=', 1, $boolean, $closure);
- }
-
- /**
- * Add a relationship count / exists condition to the query with an "or".
- *
- * @param string $relation
- * @param string $operator
- * @param int $count
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function orHas($relation, $operator = '>=', $count = 1)
- {
- return $this->has($relation, $operator, $count, 'or');
- }
-
- /**
- * Add a relationship count / exists condition to the query.
- *
- * @param string $relation
- * @param string $boolean
- * @param \Closure|null $callback
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function doesntHave($relation, $boolean = 'and', Closure $callback = null)
- {
- return $this->has($relation, '<', 1, $boolean, $callback);
- }
-
- /**
- * Add a relationship count / exists condition to the query with an "or".
- *
- * @param string $relation
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function orDoesntHave($relation)
- {
- return $this->doesntHave($relation, 'or');
- }
-
- /**
- * Add a relationship count / exists condition to the query with where clauses.
- *
- * @param string $relation
- * @param \Closure|null $callback
- * @param string $operator
- * @param int $count
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
- {
- return $this->has($relation, $operator, $count, 'and', $callback);
- }
-
- /**
- * Add a relationship count / exists condition to the query with where clauses and an "or".
- *
- * @param string $relation
- * @param \Closure $callback
- * @param string $operator
- * @param int $count
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
- {
- return $this->has($relation, $operator, $count, 'or', $callback);
- }
-
- /**
- * Add a relationship count / exists condition to the query with where clauses.
- *
- * @param string $relation
- * @param \Closure|null $callback
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function whereDoesntHave($relation, Closure $callback = null)
- {
- return $this->doesntHave($relation, 'and', $callback);
- }
-
- /**
- * Add a relationship count / exists condition to the query with where clauses and an "or".
- *
- * @param string $relation
- * @param \Closure $callback
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function orWhereDoesntHave($relation, Closure $callback = null)
- {
- return $this->doesntHave($relation, 'or', $callback);
- }
-
- /**
- * Add subselect queries to count the relations.
- *
- * @param mixed $relations
- * @return $this
- */
- public function withCount($relations)
- {
- if (empty($relations)) {
- return $this;
- }
-
- if (is_null($this->query->columns)) {
- $this->query->select([$this->query->from.'.*']);
- }
-
- $relations = is_array($relations) ? $relations : func_get_args();
-
- foreach ($this->parseWithRelations($relations) as $name => $constraints) {
- // First we will determine if the name has been aliased using an "as" clause on the name
- // and if it has we will extract the actual relationship name and the desired name of
- // the resulting column. This allows multiple counts on the same relationship name.
- $segments = explode(' ', $name);
-
- unset($alias);
-
- if (count($segments) == 3 && Str::lower($segments[1]) == 'as') {
- list($name, $alias) = [$segments[0], $segments[2]];
- }
-
- $relation = $this->getRelationWithoutConstraints($name);
-
- // Here we will get the relationship count query and prepare to add it to the main query
- // as a sub-select. First, we'll get the "has" query and use that to get the relation
- // count query. We will normalize the relation name then append _count as the name.
- $query = $relation->getRelationExistenceCountQuery(
- $relation->getRelated()->newQuery(), $this
- );
-
- $query->callScope($constraints);
-
- $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
-
- if (count($query->columns) > 1) {
- $query->columns = [$query->columns[0]];
- }
-
- // Finally we will add the proper result column alias to the query and run the subselect
- // statement against the query builder. Then we will return the builder instance back
- // to the developer for further constraint chaining that needs to take place on it.
- $column = $alias ?? Str::snake($name.'_count');
-
- $this->selectSub($query, $column);
- }
-
- return $this;
- }
-
- /**
- * Add the "has" condition where clause to the query.
- *
- * @param \Illuminate\Database\Eloquent\Builder $hasQuery
- * @param \Illuminate\Database\Eloquent\Relations\Relation $relation
- * @param string $operator
- * @param int $count
- * @param string $boolean
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean)
- {
- $hasQuery->mergeConstraintsFrom($relation->getQuery());
-
- return $this->canUseExistsForExistenceCheck($operator, $count)
- ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1)
- : $this->addWhereCountQuery($hasQuery->toBase(), $operator, $count, $boolean);
- }
-
- /**
- * Merge the where constraints from another query to the current query.
- *
- * @param \Illuminate\Database\Eloquent\Builder $from
- * @return \Illuminate\Database\Eloquent\Builder|static
- */
- public function mergeConstraintsFrom(Builder $from)
- {
- $whereBindings = $from->getQuery()->getRawBindings()['where'] ?? [];
-
- // Here we have some other query that we want to merge the where constraints from. We will
- // copy over any where constraints on the query as well as remove any global scopes the
- // query might have removed. Then we will return ourselves with the finished merging.
- return $this->withoutGlobalScopes(
- $from->removedScopes()
- )->mergeWheres(
- $from->getQuery()->wheres, $whereBindings
- );
- }
-
- /**
- * Add a sub-query count clause to this query.
- *
- * @param \Illuminate\Database\Query\Builder $query
- * @param string $operator
- * @param int $count
- * @param string $boolean
- * @return $this
- */
- protected function addWhereCountQuery(QueryBuilder $query, $operator = '>=', $count = 1, $boolean = 'and')
- {
- $this->query->addBinding($query->getBindings(), 'where');
-
- return $this->where(
- new Expression('('.$query->toSql().')'),
- $operator,
- is_numeric($count) ? new Expression($count) : $count,
- $boolean
- );
- }
-
- /**
- * Get the "has relation" base query instance.
- *
- * @param string $relation
- * @return \Illuminate\Database\Eloquent\Relations\Relation
- */
- protected function getRelationWithoutConstraints($relation)
- {
- return Relation::noConstraints(function () use ($relation) {
- return $this->getModel()->{$relation}();
- });
- }
-
- /**
- * Check if we can run an "exists" query to optimize performance.
- *
- * @param string $operator
- * @param int $count
- * @return bool
- */
- protected function canUseExistsForExistenceCheck($operator, $count)
- {
- return ($operator === '>=' || $operator === '<') && $count === 1;
- }
- }
|