newPowerJoinClause($this->query, $type, $table, $model); // If the first "column" of the join is really a Closure instance the developer // is trying to build a join with a complex "on" clause containing more than // one condition, so we'll add the join and call a Closure with the query. if ($first instanceof Closure) { $first($join); $this->query->joins[] = $join; $this->query->addBinding($join->getBindings(), 'join'); } // If the column is simply a string, we can assume the join simply has a basic // "on" clause with a single condition. So we will just build the join with // this simple join clauses attached to it. There is not a join callback. else { $method = $where ? 'where' : 'on'; $this->query->joins[] = $join->$method($first, $operator, $second); $this->query->addBinding($join->getBindings(), 'join'); } return $this; }; } /** * New clause for making joins, where we pass the model to the joiner class. */ public function leftPowerJoin(): Closure { return function ($table, $first, $operator = null, $second = null) { return $this->powerJoin($table, $first, $operator, $second, 'left'); }; } /** * New clause for making joins, where we pass the model to the joiner class. */ public function rightPowerJoin(): Closure { return function ($table, $first, $operator = null, $second = null) { return $this->powerJoin($table, $first, $operator, $second, 'right'); }; } public function newPowerJoinClause(): Closure { return function (QueryBuilder $parentQuery, $type, $table, Model $model = null) { return new PowerJoinClause($parentQuery, $type, $table, $model); }; } /** * Join the relationship(s). */ public function joinRelationship(): Closure { return function ( $relationName, $callback = null, $joinType = 'join', $useAlias = false, bool $disableExtraConditions = false, string $morphable = null ) { $joinType = JoinsHelper::$joinMethodsMap[$joinType] ?? $joinType; $useAlias = is_string($callback) ? false : $useAlias; $joinHelper = JoinsHelper::make($this->getModel()); $callback = $joinHelper->formatJoinCallback($callback); $this->getQuery()->beforeQuery(function () use ($joinHelper) { $joinHelper->clear(); }); if (is_null($this->getSelect())) { $this->select(sprintf('%s.*', $this->getModel()->getTable())); } if (Str::contains($relationName, '.')) { $this->joinNestedRelationship($relationName, $callback, $joinType, $useAlias, $disableExtraConditions); return $this; } $relationCallback = $callback; if ($callback && is_array($callback) && isset($callback[$relationName]) && is_array($callback[$relationName])) { $relationCallback = $callback[$relationName]; } $relation = $this->getModel()->{$relationName}(); $relationQuery = $relation->getQuery(); $alias = $joinHelper->getAliasName( $useAlias, $relation, $relationName, $relationQuery->getModel()->getTable(), $relationCallback ); if ($relation instanceof BelongsToMany && !is_array($alias)) { $extraAlias = $joinHelper->getAliasName( $useAlias, $relation, $relationName, $relation->getTable(), $relationCallback ); $alias = [$extraAlias, $alias]; } $aliasString = is_array($alias) ? implode('.', $alias) : $alias; $useAlias = $alias ? true : $useAlias; $relationJoinCache = $alias ? "{$aliasString}.{$relationQuery->getModel()->getTable()}.{$relationName}" : "{$relationQuery->getModel()->getTable()}.{$relationName}"; if ($joinHelper->relationshipAlreadyJoined($this->getModel(), $relationJoinCache)) { return $this; } if ($useAlias) { StaticCache::setTableAliasForModel($relation->getModel(), $alias); } $joinHelper->markRelationshipAsAlreadyJoined($this->getModel(), $relationJoinCache); StaticCache::clear(); $relation->performJoinForEloquentPowerJoins( builder: $this, joinType: $joinType, callback: $relationCallback, alias: $alias, disableExtraConditions: $disableExtraConditions, morphable: $morphable, ); return $this; }; } /** * Join the relationship(s) using table aliases. */ public function joinRelationshipUsingAlias(): Closure { return function ($relationName, $callback = null, bool $disableExtraConditions = false) { return $this->joinRelationship($relationName, $callback, 'join', true, $disableExtraConditions); }; } /** * Left join the relationship(s) using table aliases. */ public function leftJoinRelationshipUsingAlias(): Closure { return function ($relationName, $callback = null, bool $disableExtraConditions = false) { return $this->joinRelationship($relationName, $callback, 'leftJoin', true, $disableExtraConditions); }; } /** * Right join the relationship(s) using table aliases. */ public function rightJoinRelationshipUsingAlias(): Closure { return function ($relationName, $callback = null, bool $disableExtraConditions = false) { return $this->joinRelationship($relationName, $callback, 'rightJoin', true, $disableExtraConditions); }; } public function joinRelation(): Closure { return function ( $relationName, $callback = null, $joinType = 'join', $useAlias = false, bool $disableExtraConditions = false ) { return $this->joinRelationship($relationName, $callback, $joinType, $useAlias, $disableExtraConditions); }; } public function leftJoinRelationship(): Closure { return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) { return $this->joinRelationship($relation, $callback, 'leftJoin', $useAlias, $disableExtraConditions); }; } public function leftJoinRelation(): Closure { return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) { return $this->joinRelationship($relation, $callback, 'leftJoin', $useAlias, $disableExtraConditions); }; } public function rightJoinRelationship(): Closure { return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) { return $this->joinRelationship($relation, $callback, 'rightJoin', $useAlias, $disableExtraConditions); }; } public function rightJoinRelation(): Closure { return function ($relation, $callback = null, $useAlias = false, bool $disableExtraConditions = false) { return $this->joinRelationship($relation, $callback, 'rightJoin', $useAlias, $disableExtraConditions); }; } /** * Join nested relationships. */ public function joinNestedRelationship(): Closure { return function ( string $relationships, $callback = null, $joinType = 'join', $useAlias = false, bool $disableExtraConditions = false ) { $relations = explode('.', $relationships); $joinHelper = JoinsHelper::make($this->getModel()); /** @var Relation */ $latestRelation = null; $part = []; foreach ($relations as $relationName) { $part[] = $relationName; $fullRelationName = join(".", $part); $currentModel = $latestRelation ? $latestRelation->getModel() : $this->getModel(); $relation = $currentModel->{$relationName}(); $relationCallback = null; if ($callback && is_array($callback) && isset($callback[$relationName])) { $relationCallback = $callback[$relationName]; } if ($callback && is_array($callback) && isset($callback[$fullRelationName])) { $relationCallback = $callback[$fullRelationName]; } $alias = $joinHelper->getAliasName( $useAlias, $relation, $relationName, $relation->getQuery()->getModel()->getTable(), $relationCallback ); if ($alias && $relation instanceof BelongsToMany && !is_array($alias)) { $extraAlias = $joinHelper->getAliasName( $useAlias, $relation, $relationName, $relation->getTable(), $relationCallback ); $alias = [$extraAlias, $alias]; } $aliasString = is_array($alias) ? implode('.', $alias) : $alias; $useAlias = $alias ? true : $useAlias; if ($alias) { $relationJoinCache = $latestRelation ? "{$aliasString}.{$relation->getQuery()->getModel()->getTable()}.{$latestRelation->getModel()->getTable()}.{$relationName}" : "{$aliasString}.{$relation->getQuery()->getModel()->getTable()}.{$relationName}"; } else { $relationJoinCache = $latestRelation ? "{$relation->getQuery()->getModel()->getTable()}.{$latestRelation->getModel()->getTable()}.{$relationName}" : "{$relation->getQuery()->getModel()->getTable()}.{$relationName}"; } if ($useAlias) { StaticCache::setTableAliasForModel($relation->getModel(), $alias); } if ($joinHelper->relationshipAlreadyJoined($this->getModel(), $relationJoinCache)) { $latestRelation = $relation; continue; } $relation->performJoinForEloquentPowerJoins( $this, $joinType, $relationCallback, $alias, $disableExtraConditions ); $latestRelation = $relation; $joinHelper->markRelationshipAsAlreadyJoined($this->getModel(), $relationJoinCache); } StaticCache::clear(); return $this; }; } /** * Order by a field in the defined relationship. */ public function orderByPowerJoins(): Closure { return function ($sort, $direction = 'asc', $aggregation = null, $joinType = 'join') { if (is_array($sort)) { $relationships = explode('.', $sort[0]); $column = $sort[1]; $latestRelationshipName = $relationships[count($relationships) - 1]; } else { $relationships = explode('.', $sort); $column = array_pop($relationships); $latestRelationshipName = $relationships[count($relationships) - 1]; } $this->joinRelationship(implode('.', $relationships), null, $joinType); $latestRelationshipModel = array_reduce($relationships, function ($model, $relationshipName) { return $model->$relationshipName()->getModel(); }, $this->getModel()); if ($aggregation) { $aliasName = sprintf( '%s_%s_%s', $latestRelationshipModel->getTable(), $column, $aggregation ); $this->selectRaw( sprintf( '%s(%s.%s) as %s', $aggregation, $latestRelationshipModel->getTable(), $column, $aliasName ) ) ->groupBy(sprintf('%s.%s', $this->getModel()->getTable(), $this->getModel()->getKeyName())) ->orderBy(DB::raw(sprintf('%s', $aliasName)), $direction); } else { if ($column instanceof Expression) { $this->orderBy($column, $direction); } else { $this->orderBy( sprintf('%s.%s', $latestRelationshipModel->getTable(), $column), $direction ); } } return $this; }; } public function orderByLeftPowerJoins(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, null, 'leftJoin'); }; } /** * Order by the COUNT aggregation using joins. */ public function orderByPowerJoinsCount(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'COUNT'); }; } public function orderByLeftPowerJoinsCount(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'COUNT', 'leftJoin'); }; } /** * Order by the SUM aggregation using joins. */ public function orderByPowerJoinsSum(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'SUM'); }; } public function orderByLeftPowerJoinsSum(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'SUM', 'leftJoin'); }; } /** * Order by the AVG aggregation using joins. */ public function orderByPowerJoinsAvg(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'AVG'); }; } public function orderByLeftPowerJoinsAvg(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'AVG', 'leftJoin'); }; } /** * Order by the MIN aggregation using joins. */ public function orderByPowerJoinsMin(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'MIN'); }; } public function orderByLeftPowerJoinsMin(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'MIN', 'leftJoin'); }; } /** * Order by the MAX aggregation using joins. */ public function orderByPowerJoinsMax(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'MAX'); }; } public function orderByLeftPowerJoinsMax(): Closure { return function ($sort, $direction = 'asc') { return $this->orderByPowerJoins($sort, $direction, 'MAX', 'leftJoin'); }; } /** * Same as Laravel 'has`, but using joins instead of where exists. */ public function powerJoinHas(): Closure { return function ($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure|array $callback = null, string $morphable = null): static { if (is_null($this->getSelect())) { $this->select(sprintf('%s.*', $this->getModel()->getTable())); } if (is_null($this->getGroupBy())) { $this->groupBy($this->getModel()->getQualifiedKeyName()); } if (is_string($relation)) { if (Str::contains($relation, '.')) { $this->hasNestedUsingJoins($relation, $operator, $count, 'and', $callback); return $this; } $relation = $this->getRelationWithoutConstraintsProxy($relation); } $relation->performJoinForEloquentPowerJoins($this, 'leftPowerJoin', $callback, morphable: $morphable); $relation->performHavingForEloquentPowerJoins($this, $operator, $count, morphable: $morphable); return $this; }; } public function hasNestedUsingJoins(): Closure { return function ($relations, $operator = '>=', $count = 1, $boolean = 'and', Closure|array $callback = null): static { $relations = explode('.', $relations); /** @var Relation */ $latestRelation = null; foreach ($relations as $index => $relation) { $relationName = $relation; if (!$latestRelation) { $relation = $this->getRelationWithoutConstraintsProxy($relation); } else { $relation = $latestRelation->getModel()->query()->getRelationWithoutConstraintsProxy($relation); } $relation->performJoinForEloquentPowerJoins($this, 'leftPowerJoin', is_callable($callback) ? $callback : $callback[$relationName] ?? null); if (count($relations) === ($index + 1)) { $relation->performHavingForEloquentPowerJoins($this, $operator, $count); } $latestRelation = $relation; } return $this; }; } public function powerJoinDoesntHave(): Closure { return function ($relation, $boolean = 'and', Closure $callback = null) { return $this->powerJoinHas($relation, '<', 1, $boolean, $callback); }; } public function powerJoinWhereHas(): Closure { return function ($relation, $callback = null, $operator = '>=', $count = 1) { return $this->powerJoinHas($relation, $operator, $count, 'and', $callback); }; } }