$this->performJoinForEloquentPowerJoinsForMorphToMany($builder, $joinType, $callback, $alias, $disableExtraConditions), $this instanceof BelongsToMany => $this->performJoinForEloquentPowerJoinsForBelongsToMany($builder, $joinType, $callback, $alias, $disableExtraConditions), $this instanceof MorphOneOrMany => $this->performJoinForEloquentPowerJoinsForMorph($builder, $joinType, $callback, $alias, $disableExtraConditions), $this instanceof HasMany || $this instanceof HasOne => $this->performJoinForEloquentPowerJoinsForHasMany($builder, $joinType, $callback, $alias, $disableExtraConditions, $hasCheck), $this instanceof HasManyThrough || $this instanceof HasOneThrough => $this->performJoinForEloquentPowerJoinsForHasManyThrough($builder, $joinType, $callback, $alias, $disableExtraConditions), $this instanceof MorphTo => $this->performJoinForEloquentPowerJoinsForMorphTo($builder, $joinType, $callback, $alias, $disableExtraConditions, $morphable), default => $this->performJoinForEloquentPowerJoinsForBelongsTo($builder, $joinType, $callback, $alias, $disableExtraConditions), }; }; } /** * Perform the JOIN clause for the BelongsTo (or similar) relationships. */ protected function performJoinForEloquentPowerJoinsForBelongsTo() { return function ($query, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) { $joinedTable = $this->query->getModel()->getTable(); $parentTable = StaticCache::getTableOrAliasForModel($this->parent); $query->{$joinType}($joinedTable, function ($join) use ($callback, $joinedTable, $parentTable, $alias, $disableExtraConditions) { if ($alias) { $join->as($alias); } $join->on( "{$parentTable}.{$this->foreignKey}", '=', "{$joinedTable}.{$this->ownerKey}" ); if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getScopes())) { $join->whereNull("{$joinedTable}.{$this->query->getModel()->getDeletedAtColumn()}"); } if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if ($callback && is_callable($callback)) { $callback($join); } }, $this->query->getModel()); }; } /** * Perform the JOIN clause for the BelongsToMany (or similar) relationships. */ protected function performJoinForEloquentPowerJoinsForBelongsToMany() { return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) { [$alias1, $alias2] = $alias; $joinedTable = $alias1 ?: $this->getTable(); $parentTable = StaticCache::getTableOrAliasForModel($this->parent); $builder->{$joinType}($this->getTable(), function ($join) use ($callback, $joinedTable, $parentTable, $alias1) { if ($alias1) { $join->as($alias1); } $join->on( "{$joinedTable}.{$this->getForeignPivotKeyName()}", '=', "{$parentTable}.{$this->parentKey}" ); if (is_array($callback) && isset($callback[$this->getTable()])) { $callback[$this->getTable()]($join); } }); $builder->{$joinType}($this->getModel()->getTable(), function ($join) use ($callback, $joinedTable, $alias2, $disableExtraConditions) { if ($alias2) { $join->as($alias2); } $join->on( "{$this->getModel()->getTable()}.{$this->getRelatedKeyName()}", '=', "{$joinedTable}.{$this->getRelatedPivotKeyName()}" ); if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getScopes())) { $join->whereNull($this->query->getModel()->getQualifiedDeletedAtColumn()); } // applying any extra conditions to the belongs to many relationship if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if (is_array($callback) && isset($callback[$this->getModel()->getTable()])) { $callback[$this->getModel()->getTable()]($join); } }, $this->getModel()); return $this; }; } /** * Perform the JOIN clause for the MorphToMany (or similar) relationships. */ protected function performJoinForEloquentPowerJoinsForMorphToMany() { return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) { [$alias1, $alias2] = $alias; $joinedTable = $alias1 ?: $this->getTable(); $parentTable = StaticCache::getTableOrAliasForModel($this->parent); $builder->{$joinType}($this->getTable(), function ($join) use ($callback, $joinedTable, $parentTable, $alias1, $disableExtraConditions) { if ($alias1) { $join->as($alias1); } $join->on( "{$joinedTable}.{$this->getForeignPivotKeyName()}", '=', "{$parentTable}.{$this->parentKey}" ); // applying any extra conditions to the belongs to many relationship if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if (is_array($callback) && isset($callback[$this->getTable()])) { $callback[$this->getTable()]($join); } }); $builder->{$joinType}($this->getModel()->getTable(), function ($join) use ($callback, $joinedTable, $alias2, $disableExtraConditions) { if ($alias2) { $join->as($alias2); } $join->on( "{$this->getModel()->getTable()}.{$this->getModel()->getKeyName()}", '=', "{$joinedTable}.{$this->getRelatedPivotKeyName()}" ); if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getScopes())) { $join->whereNull($this->query->getModel()->getQualifiedDeletedAtColumn()); } if (is_array($callback) && isset($callback[$this->getModel()->getTable()])) { $callback[$this->getModel()->getTable()]($join); } }, $this->getModel()); return $this; }; } /** * Perform the JOIN clause for the Morph (or similar) relationships. */ protected function performJoinForEloquentPowerJoinsForMorph() { return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) { $builder->{$joinType}($this->getModel()->getTable(), function ($join) use ($callback, $disableExtraConditions, $alias) { if ($alias) { $join->as($alias); } $join->on( "{$this->getModel()->getTable()}.{$this->getForeignKeyName()}", '=', "{$this->parent->getTable()}.{$this->localKey}" )->where("{$this->getModel()->getTable()}.{$this->getMorphType()}", '=', $this->getMorphClass()); if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getScopes())) { $join->whereNull($this->query->getModel()->getQualifiedDeletedAtColumn()); } if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if ($callback && is_callable($callback)) { $callback($join); } }, $this->getModel()); return $this; }; } /** * Perform the JOIN clause for when calling the morphTo method from the morphable class. */ protected function performJoinForEloquentPowerJoinsForMorphTo() { return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false, ?string $morphable = null) { /** @var Model */ $modelInstance = new $morphable(); $builder->{$joinType}($modelInstance->getTable(), function ($join) use ($modelInstance, $callback, $disableExtraConditions) { $join->on( "{$this->getModel()->getTable()}.{$this->getForeignKeyName()}", '=', "{$modelInstance->getTable()}.{$modelInstance->getKeyName()}" )->where("{$this->getModel()->getTable()}.{$this->getMorphType()}", '=', $modelInstance->getMorphClass()); if ($disableExtraConditions === false && $this->usesSoftDeletes($modelInstance->getScopes())) { $join->whereNull($modelInstance->getQualifiedDeletedAtColumn()); } if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if ($callback && is_callable($callback)) { $callback($join); } }, $modelInstance); return $this; }; } /** * Perform the JOIN clause for the HasMany (or similar) relationships. */ protected function performJoinForEloquentPowerJoinsForHasMany() { return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false, bool $hasCheck = false) { $joinedModel = $this->query->getModel(); $joinedTable = $alias ?: $joinedModel->getTable(); $parentTable = StaticCache::getTableOrAliasForModel($this->parent); $isOneOfMany = method_exists($this, 'isOneOfMany') ? $this->isOneOfMany() : false; if ($isOneOfMany && !$hasCheck) { $column = $this->getOneOfManySubQuery()->getQuery()->columns[0]; $fkColumn = $this->getOneOfManySubQuery()->getQuery()->columns[1]; $localKey = $this->localKey; $builder->where(function ($query) use ($column, $joinType, $joinedModel, $builder, $fkColumn, $parentTable, $localKey) { $query->whereIn($joinedModel->getQualifiedKeyName(), function ($query) use ($column, $joinedModel, $builder, $fkColumn, $parentTable, $localKey) { $columnValue = $column->getValue($builder->getGrammar()); $direction = Str::contains($columnValue, 'min(') ? 'asc' : 'desc'; $columnName = Str::of($columnValue)->after('(')->before(')')->__toString(); $columnName = Str::replace(['"', "'", '`'], '', $columnName); if ($builder->getConnection() instanceof MySqlConnection) { $query->select('*')->from(function ($query) use ($joinedModel, $columnName, $fkColumn, $direction, $parentTable, $localKey) { $query ->select($joinedModel->getQualifiedKeyName()) ->from($joinedModel->getTable()) ->whereColumn($fkColumn, "{$parentTable}.{$localKey}") ->orderBy($columnName, $direction) ->take(1); }); } else { $query ->select($joinedModel->getQualifiedKeyName()) ->distinct($columnName) ->from($joinedModel->getTable()) ->whereColumn($fkColumn, "{$parentTable}.{$localKey}") ->orderBy($columnName, $direction) ->take(1); } }); if ($joinType === 'leftPowerJoin') { $query->orWhereRaw('1 = 1'); } }); } $builder->{$joinType}($this->query->getModel()->getTable(), function ($join) use ($callback, $joinedTable, $parentTable, $alias, $disableExtraConditions) { if ($alias) { $join->as($alias); } $join->on( $this->foreignKey, '=', "{$parentTable}.{$this->localKey}" ); if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getScopes())) { $join->whereNull( "{$joinedTable}.{$this->query->getModel()->getDeletedAtColumn()}" ); } if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if ($callback && is_callable($callback)) { $callback($join); } }, $this->query->getModel()); }; } /** * Perform the JOIN clause for the HasManyThrough relationships. */ protected function performJoinForEloquentPowerJoinsForHasManyThrough() { return function ($builder, $joinType, $callback = null, $alias = null, bool $disableExtraConditions = false) { [$alias1, $alias2] = $alias; $throughTable = $alias1 ?: $this->getThroughParent()->getTable(); $farTable = $alias2 ?: $this->getModel()->getTable(); $builder->{$joinType}($this->getThroughParent()->getTable(), function (PowerJoinClause $join) use ($callback, $throughTable, $alias1, $disableExtraConditions) { if ($alias1) { $join->as($alias1); } $farParentTable = StaticCache::getTableOrAliasForModel($this->getFarParent()); $join->on( "{$throughTable}.{$this->getFirstKeyName()}", '=', "{$farParentTable}.{$this->localKey}" ); if ($disableExtraConditions === false && $this->usesSoftDeletes($this->getThroughParent())) { $join->whereNull($this->getThroughParent()->getQualifiedDeletedAtColumn()); } if ($disableExtraConditions === false) { $this->applyExtraConditions($join); } if (is_array($callback) && isset($callback[$this->getThroughParent()->getTable()])) { $callback[$this->getThroughParent()->getTable()]($join); } if ($callback && is_callable($callback)) { $callback($join); } }, $this->getThroughParent()); $builder->{$joinType}($this->getModel()->getTable(), function (PowerJoinClause $join) use ($callback, $throughTable, $farTable, $alias2) { if ($alias2) { $join->as($alias2); } $join->on( "{$farTable}.{$this->secondKey}", '=', "{$throughTable}.{$this->secondLocalKey}" ); if ($this->usesSoftDeletes($this->getScopes())) { $join->whereNull("{$farTable}.{$this->getModel()->getDeletedAtColumn()}"); } if (is_array($callback) && isset($callback[$this->getModel()->getTable()])) { $callback[$this->getModel()->getTable()]($join); } }, $this->getModel()); return $this; }; } /** * Perform the "HAVING" clause for eloquent power joins. */ public function performHavingForEloquentPowerJoins() { return function ($builder, $operator, $count, ?string $morphable = null) { if ($morphable) { $modelInstance = new $morphable(); $builder ->selectRaw(sprintf('count(%s) as %s_count', $modelInstance->getQualifiedKeyName(), Str::replace('.', '_', $modelInstance->getTable()))) ->havingRaw(sprintf('count(%s) %s %d', $modelInstance->getQualifiedKeyName(), $operator, $count)); } else { $builder ->selectRaw(sprintf('count(%s) as %s_count', $this->query->getModel()->getQualifiedKeyName(), Str::replace('.', '_', $this->query->getModel()->getTable()))) ->havingRaw(sprintf('count(%s) %s %d', $this->query->getModel()->getQualifiedKeyName(), $operator, $count)); } }; } /** * Checks if the relationship model uses soft deletes. */ public function usesSoftDeletes() { /* * @param \Illuminate\Database\Eloquent\Model|array $model */ return function ($model) { if ($model instanceof Model) { return in_array(SoftDeletes::class, class_uses_recursive($model), true); } return array_key_exists(SoftDeletingScope::class, $model); }; } /** * Get the throughParent for the HasManyThrough relationship. */ public function getThroughParent() { return function () { return $this->throughParent; }; } /** * Get the farParent for the HasManyThrough relationship. */ public function getFarParent() { return function () { return $this->farParent; }; } public function applyExtraConditions() { return function (PowerJoinClause $join) { foreach ($this->getQuery()->getQuery()->wheres as $condition) { if ($this->shouldNotApplyExtraCondition($condition)) { continue; } if (!in_array($condition['type'], ['Basic', 'Null', 'NotNull', 'Nested'], true)) { continue; } $method = "apply{$condition['type']}Condition"; $this->$method($join, $condition); } }; } public function applyBasicCondition() { return function ($join, $condition) { $join->where($condition['column'], $condition['operator'], $condition['value'], $condition['boolean']); }; } public function applyNullCondition() { return function ($join, $condition) { $join->whereNull($condition['column'], $condition['boolean']); }; } public function applyNotNullCondition() { return function ($join, $condition) { $join->whereNotNull($condition['column'], $condition['boolean']); }; } public function applyNestedCondition() { return function ($join, $condition) { $join->where(function ($q) use ($condition) { foreach ($condition['query']->wheres as $condition) { $method = "apply{$condition['type']}Condition"; $this->$method($q, $condition); } }); }; } public function shouldNotApplyExtraCondition() { return function ($condition) { if (isset($condition['column']) && ($condition['column'] === '' || Str::endsWith($condition['column'], '.'))) { return true; } if (!$key = $this->getPowerJoinExistenceCompareKey()) { return true; } if (isset($condition['query'])) { return false; } if (is_array($key)) { return in_array($condition['column'], $key, true); } return $condition['column'] === $key; }; } public function getPowerJoinExistenceCompareKey() { return function () { if ($this instanceof MorphTo) { return [$this->getMorphType(), $this->getForeignKeyName()]; } if ($this instanceof BelongsTo) { return $this->getQualifiedOwnerKeyName(); } if ($this instanceof HasMany || $this instanceof HasOne) { return $this->getExistenceCompareKey(); } if ($this instanceof HasManyThrough || $this instanceof HasOneThrough) { return $this->getQualifiedFirstKeyName(); } if ($this instanceof BelongsToMany) { return $this->getExistenceCompareKey(); } if ($this instanceof MorphOneOrMany) { return [$this->getQualifiedMorphType(), $this->getExistenceCompareKey()]; } }; } }