diff --git a/src/Relations/BelongsToThrough.php b/src/Relations/BelongsToThrough.php index e424ac8..98aa285 100644 --- a/src/Relations/BelongsToThrough.php +++ b/src/Relations/BelongsToThrough.php @@ -13,55 +13,47 @@ class BelongsToThrough extends Relation { /** - * Column alias for matching eagerly loaded models. + * The column alias for the local key on the first "through" parent model. * * @var string */ - const RELATED_THROUGH_KEY = '__deep_related_through_key'; + const THROUGH_KEY = 'laravel_through_key'; /** - * List of intermediate model instances. + * The "through" parent model instances. * * @var \Illuminate\Database\Eloquent\Model[] */ - protected $models; + protected $throughParents; /** - * The local key on the relationship. + * The foreign key prefix for the first "through" parent model. * * @var string */ - protected $localKey; + protected $prefix; /** - * TODO - * - * @var string - */ - private $prefix; - - /** - * An array of table names and their foreign keys. + * The custom foreign keys on the relationship. * * @var array */ - private $foreignKeyLookup; + protected $foreignKeyLookup; /** * Create a new belongs to through relationship instance. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent - * @param \Illuminate\Database\Eloquent\Model[] $models + * @param \Illuminate\Database\Eloquent\Model[] $throughParents * @param string|null $localKey * @param string $prefix * @param array $foreignKeyLookup * @return void */ - public function __construct(Builder $query, Model $parent, array $models, $localKey = null, $prefix = '', $foreignKeyLookup = []) + public function __construct(Builder $query, Model $parent, array $throughParents, $localKey = null, $prefix = '', array $foreignKeyLookup = []) { - $this->models = $models; - $this->localKey = $localKey ?: $parent->getKeyName(); + $this->throughParents = $throughParents; $this->prefix = $prefix; $this->foreignKeyLookup = $foreignKeyLookup; @@ -75,52 +67,51 @@ public function __construct(Builder $query, Model $parent, array $models, $local */ public function addConstraints() { - $this->setJoins(); - - $this->query->select([$this->getRelated()->getTable().'.*']); + $this->query->select([$this->related->getTable().'.*']); - $this->setSoftDeletes(); + $this->performJoins(); if (static::$constraints) { - $this->query->where($this->getQualifiedParentKeyName(), '=', $this->parent[$this->localKey]); + $localValue = $this->parent[$this->getFirstForeignKeyName()]; - $this->query->whereNotNull($this->getQualifiedParentKeyName()); + $this->query->where($this->getQualifiedFirstLocalKeyName(), '=', $localValue); } } /** - * Set the required joins on the relation query. + * Set the join clauses on the query. * + * @param \Illuminate\Database\Eloquent\Builder|null $query * @return void */ - protected function setJoins() + protected function performJoins(Builder $query = null) { - $one = $this->getRelated()->getQualifiedKeyName(); - $prev = $this->getForeignKey($this->getRelated()); - $lastIndex = count($this->models) - 1; + $query = $query ?: $this->query; - foreach ($this->models as $index => $model) { - if ($lastIndex === $index) { - $prev = $this->prefix.$prev; - } + foreach ($this->throughParents as $i => $model) { + $table = $model->getTable(); + + $predecessor = $i > 0 ? $this->throughParents[$i - 1] : $this->related; - $other = $model->getTable().'.'.$prev; + $first = $table.'.'.$this->getForeignKeyName($predecessor); - $this->query->leftJoin($model->getTable(), $one, '=', $other); + $second = $predecessor->getQualifiedKeyName(); - $prev = $this->getForeignKey($model); + $query->join($table, $first, '=', $second); - $one = $model->getQualifiedKeyName(); + if ($this->hasSoftDeletes($model)) { + $this->query->whereNull($model->getQualifiedDeletedAtColumn()); + } } } /** - * Get foreign key column name for the model. + * Get the foreign key for a model. * * @param \Illuminate\Database\Eloquent\Model $model * @return string */ - protected function getForeignKey(Model $model) + protected function getForeignKeyName(Model $model) { $table = $model->getTable(); @@ -132,25 +123,7 @@ protected function getForeignKey(Model $model) } /** - * Set the soft deleting constraints on the relation query. - * - * @return void - */ - protected function setSoftDeletes() - { - foreach ($this->models as $model) { - if ($model === $this->parent) { - continue; - } - - if ($this->hasSoftDeletes($model)) { - $this->query->whereNull($model->getQualifiedDeletedAtColumn()); - } - } - } - - /** - * Determine whether the model uses Soft Deletes. + * Determine whether a model uses SoftDeletes. * * @param \Illuminate\Database\Eloquent\Model $model * @return bool @@ -169,48 +142,80 @@ public function hasSoftDeletes(Model $model) public function addEagerConstraints(array $models) { $this->query->addSelect([ - $this->getParent()->getQualifiedKeyName().' as '.static::RELATED_THROUGH_KEY, + $this->getQualifiedFirstLocalKeyName().' as '.static::THROUGH_KEY, ]); - $this->query->whereIn($this->getParent()->getQualifiedKeyName(), $this->getKeys($models, $this->localKey)); + $keys = $this->getKeys($models, $this->getFirstForeignKeyName()); + + $this->query->whereIn($this->getQualifiedFirstLocalKeyName(), $keys); } /** - * TODO + * Initialize the relation on a set of models. * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return void + * @param \Illuminate\Database\Eloquent\Model[] $models + * @param string $relation + * @return array */ - protected function setRelationQueryConstraints(Builder $query) + public function initRelation(array $models, $relation) { - $one = $this->getRelated()->getQualifiedKeyName(); - $prev = $this->getForeignKey($this->getRelated()); - $alias = null; - $lastIndex = count($this->models) - 1; - foreach ($this->models as $index => $model) { - if ($lastIndex === $index) { - $prev = $this->prefix.$prev; - } - if ($this->getParent()->getTable() === $model->getTable()) { - $alias = $model->getTable().'_'.time(); - $other = $alias.'.'.$prev; - $query->leftJoin(new Expression($model->getTable().' as '.$alias), $one, '=', $other); - } else { - $other = $model->getTable().'.'.$prev; - $query->leftJoin($model->getTable(), $one, '=', $other); + foreach ($models as $model) { + $model->setRelation($relation, null); + } + + return $models; + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param \Illuminate\Database\Eloquent\Model[] $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + $dictionary = $this->buildDictionary($results); + + foreach ($models as $model) { + $key = $model[$this->getFirstForeignKeyName()]; + + if (isset($dictionary[$key])) { + $model->setRelation($relation, $dictionary[$key]); } + } - $prev = $this->getForeignKey($model); - $one = $model->getQualifiedKeyName(); + return $models; + } + + /** + * Build model dictionary keyed by the relation's foreign key. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array + */ + protected function buildDictionary(Collection $results) + { + $dictionary = []; + + foreach ($results as $result) { + $dictionary[$result[static::THROUGH_KEY]] = $result; + + unset($result[static::THROUGH_KEY]); } - $key = $this->parent - ->newQueryWithoutScopes() - ->getQuery() - ->getGrammar() - ->wrap($this->getQualifiedParentKeyName()); + return $dictionary; + } - $query->where(new Expression($alias.'.'.$this->getParent()->getKeyName()), '=', new Expression($key)); + /** + * Get the results of the relationship. + * + * @return \Illuminate\Database\Eloquent\Model + */ + public function getResults() + { + return $this->query->first(); } /** @@ -223,11 +228,15 @@ protected function setRelationQueryConstraints(Builder $query) */ public function getRelationExistenceQuery(Builder $query, Builder $parent, $columns = ['*']) { - $query->select($columns); + $this->performJoins($query); + + $foreignKey = $parent->getQuery()->from.'.'.$this->getFirstForeignKeyName(); - $this->setRelationQueryConstraints($query); + $foreignKey = new Expression($query->getQuery()->getGrammar()->wrap($foreignKey)); - return $query; + return $query->select($columns)->where( + $this->getQualifiedFirstLocalKeyName(), '=', $foreignKey + ); } /** @@ -256,70 +265,22 @@ public function getRelationQuery(Builder $query, Builder $parent, $columns = ['* } /** - * Get the results of the relationship. + * Get the foreign key for the first "through" parent model. * - * @return \Illuminate\Database\Eloquent\Model - */ - public function getResults() - { - return $this->query->first(); - } - - /** - * Initialize the relation on a set of models. - * - * @param \Illuminate\Database\Eloquent\Model[] $models - * @param string $relation - * @return array - */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) { - $model->setRelation($relation, null); - } - - return $models; - } - - /** - * Match the eagerly loaded results to their parents. - * - * @param \Illuminate\Database\Eloquent\Model[] $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * @return array + * @return string */ - public function match(array $models, Collection $results, $relation) + public function getFirstForeignKeyName() { - $dictionary = $this->buildDictionary($results); - - foreach ($models as $model) { - $key = $model->{$this->localKey}; - - if (isset($dictionary[$key])) { - $model->setRelation($relation, $dictionary[$key]); - } - } - - return $models; + return $this->prefix.$this->getForeignKeyName(end($this->throughParents)); } /** - * Build model dictionary keyed by the relation's foreign key. + * Get the qualified local key for the first "through" parent model. * - * @param \Illuminate\Database\Eloquent\Collection $results - * @return array + * @return string */ - protected function buildDictionary(Collection $results) + public function getQualifiedFirstLocalKeyName() { - $dictionary = []; - - foreach ($results as $result) { - $dictionary[$result->{static::RELATED_THROUGH_KEY}] = $result; - - unset($result[static::RELATED_THROUGH_KEY]); - } - - return $dictionary; + return end($this->throughParents)->getQualifiedKeyName(); } } diff --git a/src/Traits/BelongsToThrough.php b/src/Traits/BelongsToThrough.php index 999938d..940bd97 100644 --- a/src/Traits/BelongsToThrough.php +++ b/src/Traits/BelongsToThrough.php @@ -22,7 +22,7 @@ public function belongsToThrough($related, $through, $localKey = null, $prefix = { /** @var \Illuminate\Database\Eloquent\Model $relatedInstance */ $relatedInstance = new $related; - $models = []; + $throughParents = []; $foreignKeys = []; foreach ((array) $through as $model) { @@ -41,11 +41,9 @@ public function belongsToThrough($related, $through, $localKey = null, $prefix = $foreignKeys[$instance->getTable()] = $foreignKey; } - $models[] = $instance; + $throughParents[] = $instance; } - $models[] = $this; - foreach ($foreignKeyLookup as $model => $foreignKey) { $instance = new $model; @@ -54,7 +52,7 @@ public function belongsToThrough($related, $through, $localKey = null, $prefix = } } - return $this->newBelongsToThrough($relatedInstance->newQuery(), $this, $models, $localKey, $prefix, $foreignKeys); + return $this->newBelongsToThrough($relatedInstance->newQuery(), $this, $throughParents, $localKey, $prefix, $foreignKeys); } /** @@ -62,14 +60,14 @@ public function belongsToThrough($related, $through, $localKey = null, $prefix = * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent - * @param \Illuminate\Database\Eloquent\Model[] $models + * @param \Illuminate\Database\Eloquent\Model[] $throughParents * @param string $localKey * @param string $prefix * @param array $foreignKeyLookup * @return \Znck\Eloquent\Relations\BelongsToThrough */ - protected function newBelongsToThrough(Builder $query, Model $parent, array $models, $localKey, $prefix, array $foreignKeyLookup) + protected function newBelongsToThrough(Builder $query, Model $parent, array $throughParents, $localKey, $prefix, array $foreignKeyLookup) { - return new Relation($query, $parent, $models, $localKey, $prefix, $foreignKeyLookup); + return new Relation($query, $parent, $throughParents, $localKey, $prefix, $foreignKeyLookup); } }