joinRelationshipCache = new WeakMap(); } public static function make($model): static { static::$instances ??= new WeakMap(); return static::$instances[$model] ??= new self(); } /** * Cache to not join the same relationship twice. */ private WeakMap $joinRelationshipCache; /** * Join method map. */ public static $joinMethodsMap = [ 'join' => 'powerJoin', 'leftJoin' => 'leftPowerJoin', 'rightJoin' => 'rightPowerJoin', ]; /** * Ensure that any query model can only belong to * maximum one query, e.g. because of cloning. */ public static function ensureModelIsUniqueToQuery($query): void { $originalModel = $query->getModel(); $querySplObjectId = spl_object_id($query); if ( isset(static::$modelQueryDictionary[$originalModel]) && static::$modelQueryDictionary[$originalModel] !== $querySplObjectId ) { // If the model is already associated with another query, we need to clone the model. // This can happen if a certain query, *before having interacted with the library // `joinRelationship()` method*, was cloned by previous code. // Preserve the from clause (including any alias) before setModel overwrites it $originalFrom = $query->getQuery()->from; $query->setModel($model = new ($query->getModel())); $model->mergeCasts($originalModel->getCasts()); if ($originalFrom) { $query->getQuery()->from = $originalFrom; } // Link the Spl Object ID of the query to the new model... static::$modelQueryDictionary[$model] = $querySplObjectId; // If there is a `JoinsHelper` with a cache associated with the old model, // we will copy the cache over to the new fresh model clone added to it. $originalJoinsHelper = JoinsHelper::make($originalModel); $joinsHelper = JoinsHelper::make($model); foreach ($originalJoinsHelper->joinRelationshipCache[$originalModel] ?? [] as $relation => $value) { $joinsHelper->markRelationshipAsAlreadyJoined($model, $relation); } } else { static::$modelQueryDictionary[$originalModel] = $querySplObjectId; } $query->onClone(static function (Builder $query) { $originalModel = $query->getModel(); $originalJoinsHelper = JoinsHelper::make($originalModel); // Preserve the from clause (including any alias) before setModel overwrites it $originalFrom = $query->getQuery()->from; // Ensure the model of the cloned query is unique to the query. $query->setModel($model = new $originalModel()); $model->mergeCasts($originalModel->getCasts()); // Restore the original from clause if it was set if ($originalFrom) { $query->getQuery()->from = $originalFrom; } // Update any `beforeQueryCallbacks` to link to the new `$this` as Eloquent Query, // otherwise the reference to the current Eloquent query goes wrong. These query // callbacks are stored on the `QueryBuilder` instance and therefore do not get // an instance of Eloquent Builder passed, but an instance of `QueryBuilder`. foreach ($query->getQuery()->beforeQueryCallbacks as $key => $beforeQueryCallback) { /** @var Closure $beforeQueryCallback */ if (isset(static::$beforeQueryCallbacks[$beforeQueryCallback])) { static::$beforeQueryCallbacks[$query->getQuery()->beforeQueryCallbacks[$key] = $beforeQueryCallback->bindTo($query)] = true; } } $joinsHelper = JoinsHelper::make($model); foreach ($originalJoinsHelper->joinRelationshipCache[$originalModel] ?? [] as $relation => $value) { $joinsHelper->markRelationshipAsAlreadyJoined($model, $relation); } }); } public static function clearCacheBeforeQuery($query): void { $beforeQueryCallback = function () { /* @var Builder $this */ JoinsHelper::make($this->getModel())->clear(); }; $query->getQuery()->beforeQuery( $beforeQueryCallback = $beforeQueryCallback->bindTo($query) ); static::$beforeQueryCallbacks[$beforeQueryCallback] = true; } /** * Format the join callback. */ 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'.uniqid('', true)), md5($relationName.'table2'.uniqid('', true)), ]; } return md5($relationName.uniqid('', true)); } /** * Get the join alias name from all the different options. */ public function getAliasName(bool $useAlias, Relation $relation, string $relationName, string $tableName, $callback): string|array|null { 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) && $relation instanceof HasOneOrManyThrough) { $alias = [null, null]; $throughParentTable = $relation->getThroughParent()->getTable(); if (isset($callback[$throughParentTable])) { $fakeJoinCallback = new FakeJoinCallback($relation->getBaseQuery(), 'inner', $throughParentTable); $callback[$throughParentTable]($fakeJoinCallback); if ($fakeJoinCallback->getAlias()) { $alias[0] = $fakeJoinCallback->getAlias(); } } $relatedTable = $relation->getRelated()->getTable(); if (isset($callback[$relatedTable])) { $fakeJoinCallback = new FakeJoinCallback($relation->getBaseQuery(), 'inner', $relatedTable); $callback[$relatedTable]($fakeJoinCallback); if ($fakeJoinCallback->getAlias()) { $alias[1] = $fakeJoinCallback->getAlias(); } } return $alias; } 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[$model][$relation]); } /** * Marks the relationship as already joined. */ public function markRelationshipAsAlreadyJoined($model, string $relation): void { $this->joinRelationshipCache[$model] ??= []; $this->joinRelationshipCache[$model][$relation] = true; } public function clear(): void { $this->joinRelationshipCache = new WeakMap(); } }