diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 14e6a85..8695812 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,7 +1,7 @@ includes: - ./vendor/larastan/larastan/extension.neon parameters: - level: 6 + level: 7 paths: - src treatPhpDocTypesAsCertain: false diff --git a/src/Eloquent/CompositeKey.php b/src/Eloquent/CompositeKey.php index dac3179..2c2af8e 100644 --- a/src/Eloquent/CompositeKey.php +++ b/src/Eloquent/CompositeKey.php @@ -14,6 +14,6 @@ class CompositeKey */ public function __construct(...$columns) { - $this->columns = $columns; + $this->columns = array_values($columns); } } diff --git a/src/Eloquent/Relations/ThirdParty/LaravelHasManyMerged/HasManyMerged.php b/src/Eloquent/Relations/ThirdParty/LaravelHasManyMerged/HasManyMerged.php index afbb5c3..36717bb 100644 --- a/src/Eloquent/Relations/ThirdParty/LaravelHasManyMerged/HasManyMerged.php +++ b/src/Eloquent/Relations/ThirdParty/LaravelHasManyMerged/HasManyMerged.php @@ -20,15 +20,13 @@ class HasManyMerged extends Base implements ConcatenableRelation /** * Append the relation's through parents, foreign and local keys to a deep relationship. * - * @param list<\Illuminate\Database\Eloquent\Model> $through - * @param list $foreignKeys - * @param list $localKeys + * @param non-empty-list $through + * @param non-empty-list $foreignKeys + * @param non-empty-list $localKeys * @param int $position - * @return array{0: list, - * 1: list, - * 2: list} + * @return array{0: non-empty-list, + * 1: non-empty-list, + * 2: non-empty-list} */ public function appendToDeepRelationship(array $through, array $foreignKeys, array $localKeys, int $position): array { diff --git a/src/Eloquent/Relations/Traits/ExecutesQueries.php b/src/Eloquent/Relations/Traits/ExecutesQueries.php index 6a1745d..0cdf8d4 100644 --- a/src/Eloquent/Relations/Traits/ExecutesQueries.php +++ b/src/Eloquent/Relations/Traits/ExecutesQueries.php @@ -23,12 +23,7 @@ public function getResults() return parent::getResults(); } - /** - * Execute the query as a "select" statement. - * - * @param string|list $columns - * @return \Illuminate\Database\Eloquent\Collection - */ + /** @inheritDoc */ public function get($columns = ['*']) { $models = parent::get($columns); @@ -111,13 +106,7 @@ public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = }); } - /** - * Chunk the results of the query. - * - * @param int $count - * @param callable $callback - * @return bool - */ + /** @inheritDoc */ public function chunk($count, callable $callback) { return $this->prepareQueryBuilder()->chunk($count, function (Collection $results) use ($callback) { diff --git a/src/Eloquent/Relations/Traits/HasEagerLoading.php b/src/Eloquent/Relations/Traits/HasEagerLoading.php index 63cddf4..aa3ff87 100644 --- a/src/Eloquent/Relations/Traits/HasEagerLoading.php +++ b/src/Eloquent/Relations/Traits/HasEagerLoading.php @@ -4,6 +4,10 @@ use Illuminate\Database\Eloquent\Collection; +/** + * @template TRelatedModel of \Illuminate\Database\Eloquent\Model + * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model + */ trait HasEagerLoading { /** @inheritDoc */ @@ -29,7 +33,14 @@ public function addEagerConstraints(array $models) } } - /** @inheritDoc */ + /** + * Match the eagerly loaded results to their parents. + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ public function match(array $models, Collection $results, $relation) { if ($this->customEagerMatchingCallbacks) { diff --git a/src/Eloquent/Relations/Traits/HasExistenceQueries.php b/src/Eloquent/Relations/Traits/HasExistenceQueries.php index 551553e..a016e03 100644 --- a/src/Eloquent/Relations/Traits/HasExistenceQueries.php +++ b/src/Eloquent/Relations/Traits/HasExistenceQueries.php @@ -17,6 +17,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, if ($this->firstKey instanceof Closure || $this->localKey instanceof Closure) { $this->performJoin($query); + /** @var callable $closureKey */ $closureKey = $this->firstKey instanceof Closure ? $this->firstKey : $this->localKey; $closureKey($query, $parentQuery); @@ -48,8 +49,14 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $query->getModel()->setTable($hash); + $from = is_string($parentQuery->getQuery()->from) + ? $parentQuery->getQuery()->from + : (string) $parentQuery->getQuery()->from->getValue( + $parentQuery->getQuery()->getGrammar() + ); + return $query->select($columns)->whereColumn( - $parentQuery->getQuery()->from.'.'.$this->localKey, + "$from.$this->localKey", '=', $this->getQualifiedFirstKeyName() ); diff --git a/src/Eloquent/Relations/Traits/IsConcatenable.php b/src/Eloquent/Relations/Traits/IsConcatenable.php index a592368..48bd2c7 100644 --- a/src/Eloquent/Relations/Traits/IsConcatenable.php +++ b/src/Eloquent/Relations/Traits/IsConcatenable.php @@ -9,13 +9,13 @@ trait IsConcatenable /** * Append the relation's through parents, foreign and local keys to a deep relationship. * - * @param list<\Illuminate\Database\Eloquent\Model> $through - * @param list $foreignKeys - * @param list $localKeys + * @param non-empty-list $through + * @param non-empty-list $foreignKeys + * @param non-empty-list $localKeys * @param int $position - * @return array{0: list, - * 1: list, - * 2: list} + * @return array{0: non-empty-list, + * 1: non-empty-list, + * 2: non-empty-list} */ public function appendToDeepRelationship(array $through, array $foreignKeys, array $localKeys, int $position): array { diff --git a/src/Eloquent/Relations/Traits/JoinsThroughParents.php b/src/Eloquent/Relations/Traits/JoinsThroughParents.php index 2a9579b..de79fe8 100644 --- a/src/Eloquent/Relations/Traits/JoinsThroughParents.php +++ b/src/Eloquent/Relations/Traits/JoinsThroughParents.php @@ -79,7 +79,7 @@ protected function throughParentJoins(Builder $query, Model $throughParent, Mode { $joins = []; - if ($localKey instanceof CompositeKey) { + if ($localKey instanceof CompositeKey && $foreignKey instanceof CompositeKey) { foreach ($localKey->columns as $i => $column) { $joins[] = [$column, $foreignKey->columns[$i]]; } diff --git a/src/Eloquent/Relations/Traits/RetrievesIntermediateTables.php b/src/Eloquent/Relations/Traits/RetrievesIntermediateTables.php index eea41cf..143ebff 100644 --- a/src/Eloquent/Relations/Traits/RetrievesIntermediateTables.php +++ b/src/Eloquent/Relations/Traits/RetrievesIntermediateTables.php @@ -11,7 +11,7 @@ trait RetrievesIntermediateTables /** * The intermediate tables to retrieve. * - * @var array, class: class-string<\Illuminate\Database\Eloquent\Model>, postProcessor: list}> + * @var array, class: class-string<\Illuminate\Database\Eloquent\Model>, postProcessor: callable|null}> */ protected $intermediateTables = []; @@ -104,7 +104,10 @@ protected function hydrateIntermediateRelations(array $models) $prefix = $this->prefix($accessor); if (str_contains($accessor, '.')) { - [$path, $key] = preg_split('/\.(?=[^.]*$)/', $accessor); + /** @var array{0: string, 1: string} $segments */ + $segments = preg_split('/\.(?=[^.]*$)/', $accessor); + + [$path, $key] = $segments; } else { [$path, $key] = [null, $accessor]; } @@ -121,7 +124,7 @@ protected function hydrateIntermediateRelations(array $models) * Get the intermediate relationship from the query. * * @param \Illuminate\Database\Eloquent\Model $model - * @param array{table: string, columns: list, class: class-string<\Illuminate\Database\Eloquent\Model>, postProcessor: list} $intermediateTable + * @param array{table: string, columns: list, class: class-string<\Illuminate\Database\Eloquent\Model>, postProcessor: callable|null} $intermediateTable * @param string $prefix * @return \Illuminate\Database\Eloquent\Model */ @@ -185,7 +188,7 @@ protected function prefix($accessor) /** * Get the intermediate tables. * - * @return array, class: class-string<\Illuminate\Database\Eloquent\Model>, postProcessor: list}> + * @return array, class: class-string<\Illuminate\Database\Eloquent\Model>, postProcessor: callable|null}> */ public function getIntermediateTables(): array { diff --git a/src/Eloquent/Relations/Traits/SupportsCompositeKeys.php b/src/Eloquent/Relations/Traits/SupportsCompositeKeys.php index ed64108..b225098 100644 --- a/src/Eloquent/Relations/Traits/SupportsCompositeKeys.php +++ b/src/Eloquent/Relations/Traits/SupportsCompositeKeys.php @@ -8,6 +8,10 @@ use Illuminate\Support\Collection as BaseCollection; use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey; +/** + * @template TRelatedModel of \Illuminate\Database\Eloquent\Model + * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model + */ trait SupportsCompositeKeys { /** @@ -27,13 +31,19 @@ protected function hasLeadingCompositeKey(): bool */ protected function addConstraintsWithCompositeKey(): void { - $columns = array_slice($this->foreignKeys[0]->columns, 1, null, true); + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $foreignKey */ + $foreignKey = $this->foreignKeys[0]; + + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $localKey */ + $localKey = $this->localKeys[0]; + + $columns = array_slice($foreignKey->columns, 1, null, true); foreach ($columns as $i => $column) { $this->query->where( $this->throughParent->qualifyColumn($column), '=', - $this->farParent[$this->localKeys[0]->columns[$i]] + $this->farParent[$localKey->columns[$i]] ); } } @@ -46,21 +56,27 @@ protected function addConstraintsWithCompositeKey(): void */ protected function addEagerConstraintsWithCompositeKey(array $models): void { + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $foreignKey */ + $foreignKey = $this->foreignKeys[0]; + + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $localKey */ + $localKey = $this->localKeys[0]; + $keys = (new BaseCollection($models))->map( - function (Model $model) { + function (Model $model) use ($localKey) { return array_map( fn (string $column) => $model[$column], - $this->localKeys[0]->columns + $localKey->columns ); } )->values()->unique(null, true)->all(); $this->query->where( - function (Builder $query) use ($keys) { + function (Builder $query) use ($foreignKey, $keys) { foreach ($keys as $key) { $query->orWhere( - function (Builder $query) use ($key) { - foreach ($this->foreignKeys[0]->columns as $i => $column) { + function (Builder $query) use ($foreignKey, $key) { + foreach ($foreignKey->columns as $i => $column) { $query->where( $this->throughParent->qualifyColumn($column), '=', @@ -77,10 +93,10 @@ function (Builder $query) use ($key) { /** * Match the eagerly loaded results to their parents for a leading composite key. * - * @param list<\Illuminate\Database\Eloquent\Model> $models - * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation - * @return list<\Illuminate\Database\Eloquent\Model> + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array */ protected function matchWithCompositeKey(array $models, Collection $results, string $relation): array { @@ -88,8 +104,10 @@ protected function matchWithCompositeKey(array $models, Collection $results, str foreach ($models as $model) { $values = []; + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $localKey */ + $localKey = $this->localKeys[0]; - foreach ($this->localKeys[0]->columns as $column) { + foreach ($localKey->columns as $column) { $values[] = $this->getDictionaryKey( $model->getAttribute($column) ); @@ -118,10 +136,13 @@ protected function buildDictionaryWithCompositeKey(Collection $results): array { $dictionary = []; + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $foreignKey */ + $foreignKey = $this->foreignKeys[0]; + foreach ($results as $result) { $values = []; - foreach ($this->foreignKeys[0]->columns as $i => $column) { + foreach ($foreignKey->columns as $i => $column) { $alias = 'laravel_through_key' . ($i > 0 ? "_$i" : ''); $values[] = $result->$alias; @@ -142,7 +163,10 @@ protected function buildDictionaryWithCompositeKey(Collection $results): array */ protected function shouldSelectWithCompositeKey(): array { - $columns = array_slice($this->foreignKeys[0]->columns, 1, null, true); + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $foreignKey */ + $foreignKey = $this->foreignKeys[0]; + + $columns = array_slice($foreignKey->columns, 1, null, true); return array_map( fn ($column, $i) => $this->throughParent->qualifyColumn($column) . " as laravel_through_key_$i", @@ -159,13 +183,19 @@ protected function shouldSelectWithCompositeKey(): array */ public function getRelationExistenceQueryWithCompositeKey(Builder $query): void { - $columns = array_slice($this->localKeys[0]->columns, 1, null, true); + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $foreignKey */ + $foreignKey = $this->foreignKeys[0]; + + /** @var \Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey $localKey */ + $localKey = $this->localKeys[0]; + + $columns = array_slice($localKey->columns, 1, null, true); foreach ($columns as $i => $column) { $query->whereColumn( $this->farParent->qualifyColumn($column), '=', - $this->throughParent->qualifyColumn($this->foreignKeys[0]->columns[$i]) + $this->throughParent->qualifyColumn($foreignKey->columns[$i]) ); } } diff --git a/src/Eloquent/Traits/ReversesRelationships.php b/src/Eloquent/Traits/ReversesRelationships.php index b94a985..93077a9 100644 --- a/src/Eloquent/Traits/ReversesRelationships.php +++ b/src/Eloquent/Traits/ReversesRelationships.php @@ -14,7 +14,7 @@ trait ReversesRelationships * * @template TRelatedModel of \Illuminate\Database\Eloquent\Model * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * TODO + * * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $relation * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep */ @@ -30,7 +30,7 @@ public function hasManyDeepFromReverse(HasManyDeep $relation): HasManyDeep * * @template TRelatedModel of \Illuminate\Database\Eloquent\Model * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * TODO + * * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $relation * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep */ @@ -46,12 +46,12 @@ public function hasOneDeepFromReverse(HasManyDeep $relation): HasOneDeep * * @template TRelatedModel of \Illuminate\Database\Eloquent\Model * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model - * TODO + * * @param \Staudenmeir\EloquentHasManyDeep\HasManyDeep $relation * @return array{0: class-string, * 1: list, - * 2: list, - * 3: list} + * 2: list, + * 3: list} */ protected function hasOneOrManyDeepFromReverse(HasManyDeep $relation): array { diff --git a/src/HasManyDeep.php b/src/HasManyDeep.php index f959269..bdadddc 100644 --- a/src/HasManyDeep.php +++ b/src/HasManyDeep.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Staudenmeir\EloquentHasManyDeep\Eloquent\CompositeKey; use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\ExecutesQueries; use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\HasEagerLoading; use Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\HasExistenceQueries; @@ -25,32 +26,34 @@ class HasManyDeep extends HasManyThrough implements ConcatenableRelation { use ExecutesQueries; + /** @use \Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\HasEagerLoading */ use HasEagerLoading; use HasExistenceQueries; use IsConcatenable; use IsCustomizable; use JoinsThroughParents; use RetrievesIntermediateTables; + /** @use \Staudenmeir\EloquentHasManyDeep\Eloquent\Relations\Traits\SupportsCompositeKeys */ use SupportsCompositeKeys; /** * The "through" parent model instances. * - * @var list<\Illuminate\Database\Eloquent\Model> + * @var non-empty-list<\Illuminate\Database\Eloquent\Model> */ protected $throughParents; /** * The foreign keys on the relationship. * - * @var list + * @var non-empty-list */ protected $foreignKeys; /** * The local keys on the relationship. * - * @var list + * @var non-empty-list */ protected $localKeys; @@ -59,9 +62,9 @@ class HasManyDeep extends HasManyThrough implements ConcatenableRelation * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $farParent - * @param list<\Illuminate\Database\Eloquent\Model> $throughParents - * @param list $foreignKeys - * @param list $localKeys + * @param non-empty-list<\Illuminate\Database\Eloquent\Model> $throughParents + * @param non-empty-list $foreignKeys + * @param non-empty-list $localKeys * @return void */ public function __construct(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys) @@ -72,10 +75,11 @@ public function __construct(Builder $query, Model $farParent, array $throughPare $firstKey = is_array($foreignKeys[0]) ? $foreignKeys[0][1] - : ($this->hasLeadingCompositeKey() ? $foreignKeys[0]->columns[0] : $foreignKeys[0]); + : ($foreignKeys[0] instanceof CompositeKey ? $foreignKeys[0]->columns[0] : $foreignKeys[0]); - $localKey = $this->hasLeadingCompositeKey() ? $localKeys[0]->columns[0] : $localKeys[0]; + $localKey = $localKeys[0] instanceof CompositeKey ? $localKeys[0]->columns[0] : $localKeys[0]; + /* @phpstan-ignore-next-line */ parent::__construct($query, $farParent, $throughParents[0], $firstKey, $foreignKeys[1], $localKey, $localKeys[1]); } @@ -114,7 +118,13 @@ protected function performJoin(?Builder $query = null) $foreignKeys = array_reverse($this->foreignKeys); $localKeys = array_reverse($this->localKeys); - $segments = explode(' as ', $query->getQuery()->from); + $from = is_string($query->getQuery()->from) + ? $query->getQuery()->from + : (string) $query->getQuery()->from->getValue( + $query->getQuery()->getGrammar() + ); + + $segments = explode(' as ', $from); $alias = $segments[1] ?? null; @@ -182,6 +192,8 @@ public function withTrashed(...$columns) $columns = $columns[0]; } + /** @var list $columns */ + foreach ($columns as $column) { $this->query->withoutGlobalScope(__CLASS__ . ":$column"); } @@ -212,7 +224,7 @@ public function getThroughParents() /** * Get the foreign keys on the relationship. * - * @return list + * @return list */ public function getForeignKeys() { @@ -222,7 +234,7 @@ public function getForeignKeys() /** * Get the local keys on the relationship. * - * @return list + * @return list */ public function getLocalKeys() { diff --git a/src/HasOneDeep.php b/src/HasOneDeep.php index 73df9a0..cd3ee04 100644 --- a/src/HasOneDeep.php +++ b/src/HasOneDeep.php @@ -16,7 +16,11 @@ class HasOneDeep extends HasManyDeep { use SupportsDefaultModels; - /** @inheritDoc */ + /** + * Get the results of the relationship. + * + * @return \Illuminate\Database\Eloquent\Model|null + */ public function getResults() { return $this->first() ?: $this->getDefaultFor(end($this->throughParents)); diff --git a/src/HasRelationships.php b/src/HasRelationships.php index b003e9b..2e3cd7b 100644 --- a/src/HasRelationships.php +++ b/src/HasRelationships.php @@ -19,10 +19,10 @@ trait HasRelationships * * @template TRelatedModel of \Illuminate\Database\Eloquent\Model * - * @param class-string $related TODO + * @param class-string $related * @param list $through - * @param list $foreignKeys - * @param list $localKeys + * @param list $foreignKeys + * @param list $localKeys * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep */ public function hasManyDeep($related, array $through, array $foreignKeys = [], array $localKeys = []) @@ -37,10 +37,10 @@ public function hasManyDeep($related, array $through, array $foreignKeys = [], a * * @template TRelatedModel of \Illuminate\Database\Eloquent\Model * - * @param class-string $related TODO + * @param class-string $related * @param list $through - * @param list $foreignKeys - * @param list $localKeys + * @param list $foreignKeys + * @param list $localKeys * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep */ public function hasOneDeep($related, array $through, array $foreignKeys = [], array $localKeys = []) @@ -60,8 +60,8 @@ public function hasOneDeep($related, array $through, array $foreignKeys = [], ar * @return array{0: \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>, * 1: $this, * 2: list<\Illuminate\Database\Eloquent\Model>, - * 3: list, - * 4: list} + * 3: list, + * 4: list} */ protected function hasOneOrManyDeep($related, array $through, array $foreignKeys, array $localKeys) { @@ -126,7 +126,7 @@ protected function newRelatedDeepThroughInstance(string $class): Model * * @param \Illuminate\Database\Eloquent\Model $related * @param list<\Illuminate\Database\Eloquent\Model> $throughParents - * @param list $foreignKeys + * @param list $foreignKeys * @return list */ protected function hasOneOrManyDeepForeignKeys(Model $related, array $throughParents, array $foreignKeys) @@ -150,7 +150,7 @@ protected function hasOneOrManyDeepForeignKeys(Model $related, array $throughPar * * @param \Illuminate\Database\Eloquent\Model $related * @param list<\Illuminate\Database\Eloquent\Model> $throughParents - * @param list $localKeys + * @param list $localKeys * @return list */ protected function hasOneOrManyDeepLocalKeys(Model $related, array $throughParents, array $localKeys) @@ -177,9 +177,9 @@ protected function hasOneOrManyDeepLocalKeys(Model $related, array $throughParen * * @param \Illuminate\Database\Eloquent\Builder $query * @param TDeclaringModel $farParent - * @param list<\Illuminate\Database\Eloquent\Model> $throughParents - * @param list $foreignKeys - * @param list $localKeys + * @param non-empty-list<\Illuminate\Database\Eloquent\Model> $throughParents + * @param non-empty-list $foreignKeys + * @param non-empty-list $localKeys * @return \Staudenmeir\EloquentHasManyDeep\HasManyDeep */ protected function newHasManyDeep(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys) @@ -195,9 +195,9 @@ protected function newHasManyDeep(Builder $query, Model $farParent, array $throu * * @param \Illuminate\Database\Eloquent\Builder $query * @param TDeclaringModel $farParent - * @param list<\Illuminate\Database\Eloquent\Model> $throughParents - * @param list $foreignKeys - * @param list $localKeys + * @param non-empty-list<\Illuminate\Database\Eloquent\Model> $throughParents + * @param non-empty-list $foreignKeys + * @param non-empty-list $localKeys * @return \Staudenmeir\EloquentHasManyDeep\HasOneDeep */ protected function newHasOneDeep(Builder $query, Model $farParent, array $throughParents, array $foreignKeys, array $localKeys)