=', $count = 1, $boolean = 'and', ?Closure $callback = null) { if (is_string($relation)) { if (str_contains($relation, '.')) { return $this->hasNested($relation, $operator, $count, $boolean, $callback); } $relation = $this->getRelationWithoutConstraints($relation); } if ($relation instanceof MorphTo) { return $this->hasMorph($relation, ['*'], $operator, $count, $boolean, $callback); } // 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()->newQueryWithoutRelationships(), $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); $doesntHave = $operator === '<' && $count === 1; if ($doesntHave) { $operator = '>='; $count = 1; } $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), $doesntHave ? '<' : '>=', 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. * * Also load the relationship with same condition. * * @param string $relation * @param \Closure|null $callback * @param string $operator * @param int $count * @return \Illuminate\Database\Eloquent\Builder|static */ public function withWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) { return $this->whereHas(Str::before($relation, ':'), $callback, $operator, $count) ->with($callback ? [$relation => fn ($query) => $callback($query)] : $relation); } /** * Add a relationship count / exists condition to the query with where clauses and an "or". * * @param string $relation * @param \Closure|null $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|null $callback * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereDoesntHave($relation, ?Closure $callback = null) { return $this->doesntHave($relation, 'or', $callback); } /** * Add a polymorphic relationship count / exists condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param string $operator * @param int $count * @param string $boolean * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static */ public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) { if (is_string($relation)) { $relation = $this->getRelationWithoutConstraints($relation); } $types = (array) $types; if ($types === ['*']) { $types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all(); } if (empty($types)) { return $this->where(new Expression('0'), $operator, $count, $boolean); } foreach ($types as &$type) { $type = Relation::getMorphedModel($type) ?? $type; } return $this->where(function ($query) use ($relation, $callback, $operator, $count, $types) { foreach ($types as $type) { $query->orWhere(function ($query) use ($relation, $callback, $operator, $count, $type) { $belongsTo = $this->getBelongsToRelation($relation, $type); if ($callback) { $callback = function ($query) use ($callback, $type) { return $callback($query, $type); }; } $query->where($this->qualifyColumn($relation->getMorphType()), '=', (new $type)->getMorphClass()) ->whereHas($belongsTo, $callback, $operator, $count); }); } }, null, null, $boolean); } /** * Get the BelongsTo relationship for a single polymorphic type. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo $relation * @param string $type * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ protected function getBelongsToRelation(MorphTo $relation, $type) { $belongsTo = Relation::noConstraints(function () use ($relation, $type) { return $this->model->belongsTo( $type, $relation->getForeignKeyName(), $relation->getOwnerKeyName() ); }); $belongsTo->getQuery()->mergeConstraintsFrom($relation->getQuery()); return $belongsTo; } /** * Add a polymorphic relationship count / exists condition to the query with an "or". * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param string $operator * @param int $count * @return \Illuminate\Database\Eloquent\Builder|static */ public function orHasMorph($relation, $types, $operator = '>=', $count = 1) { return $this->hasMorph($relation, $types, $operator, $count, 'or'); } /** * Add a polymorphic relationship count / exists condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param string $boolean * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static */ public function doesntHaveMorph($relation, $types, $boolean = 'and', ?Closure $callback = null) { return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback); } /** * Add a polymorphic relationship count / exists condition to the query with an "or". * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @return \Illuminate\Database\Eloquent\Builder|static */ public function orDoesntHaveMorph($relation, $types) { return $this->doesntHaveMorph($relation, $types, 'or'); } /** * Add a polymorphic relationship count / exists condition to the query with where clauses. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @param string $operator * @param int $count * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1) { return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback); } /** * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @param string $operator * @param int $count * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1) { return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback); } /** * Add a polymorphic relationship count / exists condition to the query with where clauses. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereDoesntHaveMorph($relation, $types, ?Closure $callback = null) { return $this->doesntHaveMorph($relation, $types, 'and', $callback); } /** * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|null $callback * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereDoesntHaveMorph($relation, $types, ?Closure $callback = null) { return $this->doesntHaveMorph($relation, $types, 'or', $callback); } /** * Add a basic where clause to a relationship query. * * @param string $relation * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereRelation($relation, $column, $operator = null, $value = null) { return $this->whereHas($relation, function ($query) use ($column, $operator, $value) { if ($column instanceof Closure) { $column($query); } else { $query->where($column, $operator, $value); } }); } /** * Add an "or where" clause to a relationship query. * * @param string $relation * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereRelation($relation, $column, $operator = null, $value = null) { return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) { if ($column instanceof Closure) { $column($query); } else { $query->where($column, $operator, $value); } }); } /** * Add a polymorphic relationship condition to the query with a where clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereMorphRelation($relation, $types, $column, $operator = null, $value = null) { return $this->whereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) { $query->where($column, $operator, $value); }); } /** * Add a polymorphic relationship condition to the query with an "or where" clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param string|array $types * @param \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column * @param mixed $operator * @param mixed $value * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereMorphRelation($relation, $types, $column, $operator = null, $value = null) { return $this->orWhereHasMorph($relation, $types, function ($query) use ($column, $operator, $value) { $query->where($column, $operator, $value); }); } /** * Add a morph-to relationship condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereMorphedTo($relation, $model, $boolean = 'and') { if (is_string($relation)) { $relation = $this->getRelationWithoutConstraints($relation); } if (is_null($model)) { return $this->whereNull($relation->getMorphType(), $boolean); } if (is_string($model)) { $morphMap = Relation::morphMap(); if (! empty($morphMap) && in_array($model, $morphMap)) { $model = array_search($model, $morphMap, true); } return $this->where($relation->getMorphType(), $model, null, $boolean); } return $this->where(function ($query) use ($relation, $model) { $query->where($relation->getMorphType(), $model->getMorphClass()) ->where($relation->getForeignKeyName(), $model->getKey()); }, null, null, $boolean); } /** * Add a not morph-to relationship condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param \Illuminate\Database\Eloquent\Model|string $model * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereNotMorphedTo($relation, $model, $boolean = 'and') { if (is_string($relation)) { $relation = $this->getRelationWithoutConstraints($relation); } if (is_string($model)) { $morphMap = Relation::morphMap(); if (! empty($morphMap) && in_array($model, $morphMap)) { $model = array_search($model, $morphMap, true); } return $this->whereNot($relation->getMorphType(), '<=>', $model, $boolean); } return $this->whereNot(function ($query) use ($relation, $model) { $query->where($relation->getMorphType(), '<=>', $model->getMorphClass()) ->where($relation->getForeignKeyName(), '<=>', $model->getKey()); }, null, null, $boolean); } /** * Add a morph-to relationship condition to the query with an "or where" clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereMorphedTo($relation, $model) { return $this->whereMorphedTo($relation, $model, 'or'); } /** * Add a not morph-to relationship condition to the query with an "or where" clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation * @param \Illuminate\Database\Eloquent\Model|string $model * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereNotMorphedTo($relation, $model) { return $this->whereNotMorphedTo($relation, $model, 'or'); } /** * Add a "belongs to" relationship where clause to the query. * * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model> $related * @param string|null $relationshipName * @param string $boolean * @return $this * * @throws \Illuminate\Database\Eloquent\RelationNotFoundException */ public function whereBelongsTo($related, $relationshipName = null, $boolean = 'and') { if (! $related instanceof Collection) { $relatedCollection = $related->newCollection([$related]); } else { $relatedCollection = $related; $related = $relatedCollection->first(); } if ($relatedCollection->isEmpty()) { throw new InvalidArgumentException('Collection given to whereBelongsTo method may not be empty.'); } if ($relationshipName === null) { $relationshipName = Str::camel(class_basename($related)); } try { $relationship = $this->model->{$relationshipName}(); } catch (BadMethodCallException) { throw RelationNotFoundException::make($this->model, $relationshipName); } if (! $relationship instanceof BelongsTo) { throw RelationNotFoundException::make($this->model, $relationshipName, BelongsTo::class); } $this->whereIn( $relationship->getQualifiedForeignKeyName(), $relatedCollection->pluck($relationship->getOwnerKeyName())->toArray(), $boolean, ); return $this; } /** * Add an "BelongsTo" relationship with an "or where" clause to the query. * * @param \Illuminate\Database\Eloquent\Model $related * @param string|null $relationshipName * @return $this * * @throws \RuntimeException */ public function orWhereBelongsTo($related, $relationshipName = null) { return $this->whereBelongsTo($related, $relationshipName, 'or'); } /** * Add subselect queries to include an aggregate value for a relationship. * * @param mixed $relations * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @param string $function * @return $this */ public function withAggregate($relations, $column, $function = null) { if (empty($relations)) { return $this; } if (is_null($this->query->columns)) { $this->query->select([$this->query->from.'.*']); } $relations = is_array($relations) ? $relations : [$relations]; 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 aggregates on the same relationships. $segments = explode(' ', $name); unset($alias); if (count($segments) === 3 && Str::lower($segments[1]) === 'as') { [$name, $alias] = [$segments[0], $segments[2]]; } $relation = $this->getRelationWithoutConstraints($name); if ($function) { if ($this->getQuery()->getGrammar()->isExpression($column)) { $aggregateColumn = $this->getQuery()->getGrammar()->getValue($column); } else { $hashedColumn = $this->getRelationHashedColumn($column, $relation); $aggregateColumn = $this->getQuery()->getGrammar()->wrap( $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) ); } $expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn); } else { $expression = $this->getQuery()->getGrammar()->getValue($column); } // Here, we will grab the relationship sub-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 // sub-query. We'll format this relationship name and append this column if needed. $query = $relation->getRelationExistenceQuery( $relation->getRelated()->newQuery(), $this, new Expression($expression) )->setBindings([], 'select'); $query->callScope($constraints); $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase(); // If the query contains certain elements like orderings / more than one column selected // then we will remove those elements from the query so that it will execute properly // when given to the database. Otherwise, we may receive SQL errors or poor syntax. $query->orders = null; $query->setBindings([], 'order'); if (count($query->columns) > 1) { $query->columns = [$query->columns[0]]; $query->bindings['select'] = []; } // Finally, we will make the proper column alias to the query and run this sub-select on // the query builder. Then, we will return the builder instance back to the developer // for further constraint chaining that needs to take place on the query as needed. $alias ??= Str::snake( preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function {$this->getQuery()->getGrammar()->getValue($column)}") ); if ($function === 'exists') { $this->selectRaw( sprintf('exists(%s) as %s', $query->toSql(), $this->getQuery()->grammar->wrap($alias)), $query->getBindings() )->withCasts([$alias => 'bool']); } else { $this->selectSub( $function ? $query : $query->limit(1), $alias ); } } return $this; } /** * Get the relation hashed column name for the given column and relation. * * @param string $column * @param \Illuminate\Database\Eloquent\Relations\Relation $relation * @return string */ protected function getRelationHashedColumn($column, $relation) { if (str_contains($column, '.')) { return $column; } return $this->getQuery()->from === $relation->getQuery()->getQuery()->from ? "{$relation->getRelationCountHash(false)}.$column" : $column; } /** * Add subselect queries to count the relations. * * @param mixed $relations * @return $this */ public function withCount($relations) { return $this->withAggregate(is_array($relations) ? $relations : func_get_args(), '*', 'count'); } /** * Add subselect queries to include the max of the relation's column. * * @param string|array $relation * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withMax($relation, $column) { return $this->withAggregate($relation, $column, 'max'); } /** * Add subselect queries to include the min of the relation's column. * * @param string|array $relation * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withMin($relation, $column) { return $this->withAggregate($relation, $column, 'min'); } /** * Add subselect queries to include the sum of the relation's column. * * @param string|array $relation * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withSum($relation, $column) { return $this->withAggregate($relation, $column, 'sum'); } /** * Add subselect queries to include the average of the relation's column. * * @param string|array $relation * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withAvg($relation, $column) { return $this->withAggregate($relation, $column, 'avg'); } /** * Add subselect queries to include the existence of related models. * * @param string|array $relation * @return $this */ public function withExists($relation) { return $this->withAggregate($relation, '*', 'exists'); } /** * 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'] ?? []; $wheres = $from->getQuery()->from !== $this->getQuery()->from ? $this->requalifyWhereTables( $from->getQuery()->wheres, $from->getQuery()->grammar->getValue($from->getQuery()->from), $this->getModel()->getTable() ) : $from->getQuery()->wheres; // 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( $wheres, $whereBindings ); } /** * Updates the table name for any columns with a new qualified name. * * @param array $wheres * @param string $from * @param string $to * @return array */ protected function requalifyWhereTables(array $wheres, string $from, string $to): array { return collect($wheres)->map(function ($where) use ($from, $to) { return collect($where)->map(function ($value) use ($from, $to) { return is_string($value) && str_starts_with($value, $from.'.') ? $to.'.'.Str::afterLast($value, '.') : $value; }); })->toArray(); } /** * 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; } }