[增添]添加了datasource的setting数据库以及默认值

This commit is contained in:
makotocc0107
2024-08-27 09:57:44 +08:00
parent d111dfaea4
commit 72eb990970
10955 changed files with 978898 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Database\Eloquent\Builder as EloquentQueryBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Kirschbaum\PowerJoins\Mixins\JoinRelationship;
use Kirschbaum\PowerJoins\Mixins\QueryBuilderExtraMethods;
use Kirschbaum\PowerJoins\Mixins\QueryRelationshipExistence;
use Kirschbaum\PowerJoins\Mixins\RelationshipsExtraMethods;
class EloquentJoins
{
/**
* Register macros with Eloquent.
*/
public static function registerEloquentMacros()
{
EloquentQueryBuilder::mixin(new JoinRelationship);
EloquentQueryBuilder::mixin(new QueryRelationshipExistence);
QueryBuilder::mixin(new QueryBuilderExtraMethods);
Relation::mixin(new RelationshipsExtraMethods);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Kirschbaum\PowerJoins;
/**
* @method static as(string $alias)
*/
class FakeJoinCallback extends PowerJoinClause
{
public function getAlias(): ?string
{
return $this->alias;
}
public function __call($name, $arguments)
{
if ($name === 'as') {
$this->alias = $arguments[0];
}
return $this;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\Relation;
class JoinsHelper
{
static array $instances = [];
protected function __construct()
{
}
public static function make(): static
{
$objects = array_map(fn ($object) => spl_object_id($object), func_get_args());
return static::$instances[implode('-', $objects)] ??= new self();
}
/**
* Cache to not join the same relationship twice.
*
* @var array
*/
private array $joinRelationshipCache = [];
/**
* Join method map.
*/
public static $joinMethodsMap = [
'join' => 'powerJoin',
'leftJoin' => 'leftPowerJoin',
'rightJoin' => 'rightPowerJoin',
];
/**
* Format the join callback.
*
* @param mixed $callback
* @return mixed
*/
public function formatJoinCallback($callback)
{
if (is_string($callback)) {
return function ($join) use ($callback) {
$join->as($callback);
};
}
return $callback;
}
public function generateAliasForRelationship(Relation $relation, string $relationName): array|string
{
if ($relation instanceof BelongsToMany || $relation instanceof HasManyThrough) {
return [
md5($relationName.'table1'.time()),
md5($relationName.'table2'.time()),
];
}
return md5($relationName.time());
}
/**
* Get the join alias name from all the different options.
*/
public function getAliasName(bool $useAlias, Relation $relation, string $relationName, string $tableName, $callback): null|string|array
{
if ($callback) {
if (is_callable($callback)) {
$fakeJoinCallback = new FakeJoinCallback($relation->getBaseQuery(), 'inner', $tableName);
$callback($fakeJoinCallback);
if ($fakeJoinCallback->getAlias()) {
return $fakeJoinCallback->getAlias();
}
}
if (is_array($callback) && isset($callback[$tableName])) {
$fakeJoinCallback = new FakeJoinCallback($relation->getBaseQuery(), 'inner', $tableName);
$callback[$tableName]($fakeJoinCallback);
if ($fakeJoinCallback->getAlias()) {
return $fakeJoinCallback->getAlias();
}
}
}
return $useAlias
? $this->generateAliasForRelationship($relation, $relationName)
: null;
}
/**
* Checks if the relationship was already joined.
*/
public function relationshipAlreadyJoined($model, string $relation): bool
{
return isset($this->joinRelationshipCache[spl_object_id($model)][$relation]);
}
/**
* Marks the relationship as already joined.
*/
public function markRelationshipAsAlreadyJoined($model, string $relation): void
{
$this->joinRelationshipCache[spl_object_id($model)][$relation] = true;
}
public function clear(): void
{
$this->joinRelationshipCache = [];
}
}

View File

@@ -0,0 +1,560 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Kirschbaum\PowerJoins\JoinsHelper;
use Kirschbaum\PowerJoins\PowerJoinClause;
use Kirschbaum\PowerJoins\StaticCache;
/**
* @mixin Builder
* @method \Illuminate\Database\Eloquent\Model getModel()
* @property \Illuminate\Database\Eloquent\Builder $query
*/
class JoinRelationship
{
/**
* New clause for making joins, where we pass the model to the joiner class.
*/
public function powerJoin(): Closure
{
return function ($table, $first, $operator = null, $second = null, $type = 'inner', $where = false): static {
$model = $operator instanceof Model ? $operator : null;
$join = $this->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);
};
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
class QueryBuilderExtraMethods
{
public function getGroupBy()
{
return function () {
return $this->groups;
};
}
public function getSelect()
{
return function () {
return $this->columns;
};
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
use Illuminate\Database\Eloquent\Relations\Relation;
class QueryRelationshipExistence
{
public function getGroupBy()
{
return function () {
return $this->getQuery()->getGroupBy();
};
}
public function getSelect()
{
return function () {
return $this->getQuery()->getSelect();
};
}
protected function getRelationWithoutConstraintsProxy()
{
return function ($relation) {
return Relation::noConstraints(function () use ($relation) {
return $this->getModel()->{$relation}();
});
};
}
}

View File

@@ -0,0 +1,538 @@
<?php
namespace Kirschbaum\PowerJoins\Mixins;
use Stringable;
use Illuminate\Support\Str;
use Kirschbaum\PowerJoins\StaticCache;
use Kirschbaum\PowerJoins\PowerJoinClause;
use Kirschbaum\PowerJoins\Tests\Models\Post;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphOneOrMany;
/**
* @method \Illuminate\Database\Eloquent\Model getModel()
* @method string getTable()
* @method string getForeignPivotKeyName()
* @method string getRelatedPivotKeyName()
* @method bool isOneOfMany()
* @method \Illuminate\Database\Eloquent\Builder|void getOneOfManySubQuery()
* @method \Illuminate\Database\Eloquent\Builder getQuery()
* @method \Illuminate\Database\Eloquent\Model getThroughParent()
* @method string getForeignKeyName()
* @method string getMorphType()
* @method string getMorphClass()
* @method string getFirstKeyName()
* @method string getQualifiedLocalKeyName()
* @method string getExistenceCompareKey()
* @mixin \Illuminate\Database\Eloquent\Relations\Relation
* @mixin \Illuminate\Database\Eloquent\Relations\HasOneOrMany
* @mixin \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @property \Illuminate\Database\Eloquent\Builder $query
* @property \Illuminate\Database\Eloquent\Model $parent
* @property \Illuminate\Database\Eloquent\Model $throughParent
* @property string $foreignKey
* @property string $parentKey
* @property string $ownerKey
* @property string $localKey
* @property string $secondKey
* @property string $secondLocalKey
* @property \Illuminate\Database\Eloquent\Model $farParent
*/
class RelationshipsExtraMethods
{
/**
* Perform the JOIN clause for eloquent power joins.
*/
public function performJoinForEloquentPowerJoins()
{
return function ($builder, $joinType = 'leftJoin', $callback = null, $alias = null, bool $disableExtraConditions = false, string $morphable = null) {
return match (true) {
$this instanceof MorphToMany => $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),
$this instanceof HasManyThrough => $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->getModel())) {
$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->getModel()->getKeyName()}",
'=',
"{$joinedTable}.{$this->getRelatedPivotKeyName()}"
);
if ($disableExtraConditions === false && $this->usesSoftDeletes($this->query->getModel())) {
$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->getModel())) {
$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) {
$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->getModel())) {
$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) {
$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)) {
$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) {
$joinedTable = $alias ?: $this->query->getModel()->getTable();
$parentTable = StaticCache::getTableOrAliasForModel($this->parent);
$isOneOfMany = method_exists($this, 'isOneOfMany') ? $this->isOneOfMany() : false;
if ($isOneOfMany) {
foreach ($this->getOneOfManySubQuery()->getQuery()->columns as $column) {
$builder->addSelect($column);
}
$builder->take(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->getModel())) {
$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);
}
$join->on(
"{$throughTable}.{$this->getFirstKeyName()}",
'=',
$this->getQualifiedLocalKeyName()
);
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, $alias1, $alias2) {
if ($alias2) {
$join->as($alias2);
}
$join->on(
"{$farTable}.{$this->secondKey}",
'=',
"{$throughTable}.{$this->secondLocalKey}"
);
if ($this->usesSoftDeletes($this->getModel())) {
$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()
{
return function ($model) {
return in_array(SoftDeletes::class, class_uses_recursive($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'])) {
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']) && 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);
}
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) {
return $this->getQualifiedFirstKeyName();
}
if ($this instanceof BelongsToMany) {
return $this->getExistenceCompareKey();
}
if ($this instanceof MorphOneOrMany) {
return [$this->getQualifiedMorphType(), $this->getExistenceCompareKey()];
}
};
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Kirschbaum\PowerJoins;
use Closure;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class PowerJoinClause extends JoinClause
{
/**
* @var \Illuminate\Database\Eloquent\Model
*/
public $model;
/**
* Table name backup in case an alias is being used.
*
* @var string
*/
public $tableName;
/**
* Alias name.
*/
public ?string $alias = null;
/**
* Joined table alias name (mostly for belongs to many aliases).
*/
public ?string $joinedTableAlias = null;
/**
* Create a new join clause instance.
*/
public function __construct(Builder $parentQuery, $type, string $table, Model $model = null)
{
parent::__construct($parentQuery, $type, $table);
$this->model = $model;
$this->tableName = $table;
}
/**
* Add an alias to the table being joined.
*/
public function as(string $alias, ?string $joinedTableAlias = null): self
{
$this->alias = $alias;
$this->joinedTableAlias = $joinedTableAlias;
$this->table = sprintf('%s as %s', $this->table, $alias);
$this->useTableAliasInConditions();
if ($this->model) {
StaticCache::setTableAliasForModel($this->model, $alias);
}
return $this;
}
public function on($first, $operator = null, $second = null, $boolean = 'and'): self
{
parent::on($first, $operator, $second, $boolean);
$this->useTableAliasInConditions();
return $this;
}
public function getModel()
{
return $this->model;
}
/**
* Apply the global scopes to the joined query.
*/
public function withGlobalScopes(): self
{
if (! $this->model) {
return $this;
}
foreach ($this->model->getGlobalScopes() as $scope) {
if ($scope instanceof Closure) {
$scope->call($this, $this);
continue;
}
if ($scope instanceof SoftDeletingScope) {
continue;
}
(new $scope())->apply($this, $this->model);
}
return $this;
}
/**
* Apply the table alias in the existing join conditions.
*/
protected function useTableAliasInConditions(): self
{
if (! $this->alias || ! $this->model) {
return $this;
}
$this->wheres = collect($this->wheres)->filter(function ($where) {
return in_array($where['type'] ?? '', ['Column', 'Basic']);
})->map(function ($where) {
$key = $this->model->getKeyName();
$table = $this->tableName;
$replaceMethod = sprintf('useAliasInWhere%sType', ucfirst($where['type']));
return $this->{$replaceMethod}($where);
})->toArray();
return $this;
}
protected function useAliasInWhereColumnType(array $where): array
{
$key = $this->model->getKeyName();
$table = $this->tableName;
// if it was already replaced, skip
if (Str::startsWith($where['first'] . '.', $this->alias . '.') || Str::startsWith($where['second'] . '.', $this->alias . '.')) {
return $where;
}
if (Str::contains($where['first'], $table) && Str::contains($where['second'], $table)) {
// if joining the same table, only replace the correct table.key pair
$where['first'] = str_replace($table . '.' . $key, $this->alias . '.' . $key, $where['first']);
$where['second'] = str_replace($table . '.' . $key, $this->alias . '.' . $key, $where['second']);
} else {
$where['first'] = str_replace($table . '.', $this->alias . '.', $where['first']);
$where['second'] = str_replace($table . '.', $this->alias . '.', $where['second']);
}
return $where;
}
protected function useAliasInWhereBasicType(array $where): array
{
$table = $this->tableName;
if (Str::startsWith($where['column'] . '.', $this->alias . '.')) {
return $where;
}
if (Str::contains($where['column'], $table)) {
// if joining the same table, only replace the correct table.key pair
$where['column'] = str_replace($table . '.', $this->alias . '.', $where['column']);
} else {
$where['column'] = str_replace($table . '.', $this->alias . '.', $where['column']);
}
return $where;
}
public function whereNull($columns, $boolean = 'and', $not = false)
{
if ($this->alias && Str::contains($columns, $this->tableName)) {
$columns = str_replace("{$this->tableName}.", "{$this->alias}.", $columns);
}
return parent::whereNull($columns, $boolean, $not);
}
public function newQuery(): self
{
return new static($this->newParentQuery(), $this->type, $this->table, $this->model); // <-- The model param is needed
}
public function where($column, $operator = null, $value = null, $boolean = 'and'): self
{
if ($this->alias && is_string($column) && Str::contains($column, $this->tableName)) {
$column = str_replace("{$this->tableName}.", "{$this->alias}.", $column);
} elseif ($this->alias && ! is_callable($column)) {
$column = $this->alias . '.' . $column;
}
if (is_callable($column)) {
$query = new self($this, $this->type, $this->table, $this->model);
$column($query);
return $this->addNestedWhereQuery($query);
} else {
return parent::where($column, $operator, $value, $boolean);
}
}
/**
* Remove the soft delete condition in case the model implements soft deletes.
*/
public function withTrashed(): self
{
if (! $this->getModel() || ! in_array(SoftDeletes::class, class_uses_recursive($this->getModel()))) {
return $this;
}
$this->wheres = array_filter($this->wheres, function ($where) {
if ($where['type'] === 'Null' && Str::contains($where['column'], $this->getModel()->getDeletedAtColumn())) {
return false;
}
return true;
});
return $this;
}
/**
* Remove the soft delete condition in case the model implements soft deletes.
*/
public function onlyTrashed(): self
{
if (! $this->getModel() || ! in_array(SoftDeletes::class, class_uses_recursive($this->getModel()))) {
return $this;
}
$this->wheres = array_map(function ($where) {
if ($where['type'] === 'Null' && Str::contains($where['column'], $this->getModel()->getDeletedAtColumn())) {
$where['type'] = 'NotNull';
}
return $where;
}, $this->wheres);
return $this;
}
public function __call($name, $arguments)
{
$scope = 'scope' . ucfirst($name);
if (! $this->getModel()) {
return;
}
if (method_exists($this->getModel(), $scope)) {
return $this->getModel()->{$scope}($this, ...$arguments);
} else {
if (static::hasMacro($name)) {
return $this->macroCall($name, $arguments);
}
$eloquentBuilder = $this->getModel()->newEloquentBuilder($this);
if (method_exists($eloquentBuilder, $name)) {
return $eloquentBuilder->{$name}(...$arguments);
}
throw new InvalidArgumentException(sprintf('Method %s does not exist in PowerJoinClause class', $name));
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Kirschbaum\PowerJoins;
trait PowerJoins
{
//
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Support\ServiceProvider;
class PowerJoinsServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot()
{
}
/**
* Register the application services.
*/
public function register()
{
EloquentJoins::registerEloquentMacros();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Kirschbaum\PowerJoins;
use Illuminate\Database\Eloquent\Model;
class StaticCache
{
/**
* Cache to not join the same relationship twice.
* @var array<int, string>
*/
public static array $powerJoinAliasesCache = [];
public static function getTableOrAliasForModel(Model $model): string
{
return static::$powerJoinAliasesCache[spl_object_id($model)] ?? $model->getTable();
}
public static function setTableAliasForModel(Model $model, $alias): void
{
static::$powerJoinAliasesCache[spl_object_id($model)] = $alias;
}
public static function clear(): void
{
static::$powerJoinAliasesCache = [];
}
}