Skip to content

Commit

Permalink
Improve TIntermediateModels generics and add inheritDoc (#102)
Browse files Browse the repository at this point in the history
* Relation generics and PHPStan lvl 9

* Resolve TODO

* Refactoring

* Support PHPStan v1.12.1

* Apply review feedback of @calebdw

* Resolve phpstan errors

* Apply review feedback

* Update tests/Models/Comment.php

Co-authored-by: Caleb White <[email protected]>

* Handle end return type without docblock

* Resolve phpstan errors

* Update BelongsToThrough.php

* Revert PHPStan version constraint after fix

* Fix phpstan errors by removing TIntermediateModels

* Refactoring

* Improve code coverage

---------

Co-authored-by: Jonas Staudenmeir <[email protected]>
Co-authored-by: Caleb White <[email protected]>
  • Loading branch information
3 people authored Sep 9, 2024
1 parent 8332f9d commit 28cbfca
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 69 deletions.
76 changes: 25 additions & 51 deletions src/Relations/BelongsToThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Str;
use RuntimeException;

/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>
Expand All @@ -32,7 +32,7 @@ class BelongsToThrough extends Relation
/**
* The "through" parent model instances.
*
* @var TIntermediateModel[]
* @var list<\Illuminate\Database\Eloquent\Model>
*/
protected $throughParents;

Expand Down Expand Up @@ -62,7 +62,7 @@ class BelongsToThrough extends Relation
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param TIntermediateModel[] $throughParents
* @param list<\Illuminate\Database\Eloquent\Model> $throughParents
* @param string|null $localKey
* @param string $prefix
* @param array<string, string> $foreignKeyLookup
Expand All @@ -88,11 +88,7 @@ public function __construct(
parent::__construct($query, $parent);
}

/**
* Set the base constraints on the relation query.
*
* @return void
*/
/** @inheritDoc */
public function addConstraints()
{
$this->performJoins();
Expand Down Expand Up @@ -179,26 +175,15 @@ public function hasSoftDeletes(Model $model)
return in_array(SoftDeletes::class, class_uses_recursive($model));
}

/**
* Set the constraints for an eager load of the relation.
*
* @param array<int, TDeclaringModel> $models
* @return void
*/
/** @inheritDoc */
public function addEagerConstraints(array $models)
{
$keys = $this->getKeys($models, $this->getFirstForeignKeyName());

$this->query->whereIn($this->getQualifiedFirstLocalKeyName(), $keys);
}

/**
* Initialize the relation on a set of models.
*
* @param array<int, TDeclaringModel> $models
* @param string $relation
* @return array<int, TDeclaringModel>
*/
/** @inheritDoc */
public function initRelation(array $models, $relation)
{
foreach ($models as $model) {
Expand All @@ -208,14 +193,7 @@ public function initRelation(array $models, $relation)
return $models;
}

/**
* Match the eagerly loaded results to their parents.
*
* @param array<int, TDeclaringModel> $models
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @param string $relation
* @return array<int, TDeclaringModel>
*/
/** @inheritDoc */
public function match(array $models, Collection $results, $relation)
{
$dictionary = $this->buildDictionary($results);
Expand All @@ -234,8 +212,8 @@ public function match(array $models, Collection $results, $relation)
/**
* Build model dictionary keyed by the relation's foreign key.
*
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $results
* @return \Illuminate\Database\Eloquent\Model[]
* @param \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> $results
* @return TRelatedModel[]
*/
protected function buildDictionary(Collection $results)
{
Expand Down Expand Up @@ -275,12 +253,7 @@ public function first($columns = ['*'])
return $this->query->first($columns);
}

/**
* Execute the query as a "select" statement.
*
* @param string[] $columns
* @return \Illuminate\Database\Eloquent\Collection<array-key, TRelatedModel>
*/
/** @inheritDoc */
public function get($columns = ['*'])
{
$columns = $this->query->getQuery()->columns ? [] : $columns;
Expand All @@ -296,14 +269,7 @@ public function get($columns = ['*'])
return $this->query->get();
}

/**
* Add the constraints for a relationship query.
*
* @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $query
* @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $parentQuery
* @param string[]|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>
*/
/** @inheritDoc */
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$this->performJoins($query);
Expand All @@ -318,7 +284,7 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery,

$foreignKey = $from . '.' . $this->getFirstForeignKeyName();

/** @var \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $query */
/** @var \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query */
$query = $query->select($columns)->whereColumn(
$this->getQualifiedFirstLocalKeyName(),
'=',
Expand Down Expand Up @@ -358,7 +324,7 @@ public function withTrashed(...$columns)
/**
* Get the "through" parent model instances.
*
* @return TIntermediateModel[]
* @return list<\Illuminate\Database\Eloquent\Model>
*/
public function getThroughParents()
{
Expand All @@ -372,9 +338,13 @@ public function getThroughParents()
*/
public function getFirstForeignKeyName()
{
/** @var TIntermediateModel $firstThroughParent */
$firstThroughParent = end($this->throughParents);

if ($firstThroughParent === false) {
// @codeCoverageIgnore
throw new RuntimeException('No "through" parent models were specified.');
}

return $this->prefix . $this->getForeignKeyName($firstThroughParent);
}

Expand All @@ -385,10 +355,14 @@ public function getFirstForeignKeyName()
*/
public function getQualifiedFirstLocalKeyName()
{
/** @var TIntermediateModel $lastThroughParent */
$lastThroughParent = end($this->throughParents);
$firstThroughParent = end($this->throughParents);

if ($firstThroughParent === false) {
// @codeCoverageIgnore
throw new RuntimeException('No "through" parent models were specified.');
}

return $lastThroughParent->qualifyColumn($this->getLocalKeyName($lastThroughParent));
return $firstThroughParent->qualifyColumn($this->getLocalKeyName($firstThroughParent));
}

/**
Expand Down
15 changes: 6 additions & 9 deletions src/Traits/BelongsToThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ trait BelongsToThrough
* Define a belongs-to-through relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TRelatedModel> $related
* @param class-string<TIntermediateModel>[]|array{0: class-string<TIntermediateModel>, 1: string}[]|class-string<TIntermediateModel> $through
* @param class-string<\Illuminate\Database\Eloquent\Model>[]|array{0: class-string<\Illuminate\Database\Eloquent\Model>, 1: string}[]|class-string<\Illuminate\Database\Eloquent\Model> $through
* @param string|null $localKey
* @param string $prefix
* @param array<class-string<\Illuminate\Database\Eloquent\Model>, string> $foreignKeyLookup
* @param array<class-string<\Illuminate\Database\Eloquent\Model>, string> $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough<TRelatedModel, TIntermediateModel, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<TRelatedModel, $this>
*/
public function belongsToThrough(
$related,
Expand All @@ -33,7 +32,7 @@ public function belongsToThrough(
/** @var TRelatedModel $relatedInstance */
$relatedInstance = $this->newRelatedInstance($related);

/** @var TIntermediateModel[] $throughParents */
/** @var list<\Illuminate\Database\Eloquent\Model> $throughParents */
$throughParents = [];
$foreignKeys = [];

Expand All @@ -44,11 +43,10 @@ public function belongsToThrough(
/** @var string $foreignKey */
$foreignKey = $model[1];

/** @var class-string<TIntermediateModel> $model */
/** @var class-string<\Illuminate\Database\Eloquent\Model> $model */
$model = $model[0];
}

/** @var TIntermediateModel $instance */
$instance = $this->belongsToThroughParentInstance($model);

if ($foreignKey) {
Expand Down Expand Up @@ -122,17 +120,16 @@ protected function belongsToThroughParentInstance($model)
* Instantiate a new BelongsToThrough relationship.
*
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @template TIntermediateModel of \Illuminate\Database\Eloquent\Model
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param TIntermediateModel[] $throughParents
* @param list<\Illuminate\Database\Eloquent\Model> $throughParents
* @param string|null $localKey
* @param string $prefix
* @param array<string, string> $foreignKeyLookup
* @param array<string, string> $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* @return \Znck\Eloquent\Relations\BelongsToThrough<TRelatedModel, TDeclaringModel>
*/
protected function newBelongsToThrough(
Builder $query,
Expand Down
2 changes: 1 addition & 1 deletion tests/IdeHelper/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Comment extends Model
use BelongsToThroughTrait;

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\IdeHelper\Models\Country, \Tests\IdeHelper\Models\User|\Tests\IdeHelper\Models\Post, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\IdeHelper\Models\Country, $this>
*/
public function country(): BelongsToThroughRelation
{
Expand Down
14 changes: 7 additions & 7 deletions tests/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ class Comment extends Model
use HasTableAlias;

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, \Tests\Models\User|\Tests\Models\Post, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, $this>
*/
public function country(): BelongsToThrough
{
return $this->belongsToThrough(Country::class, [User::class, Post::class])->withDefault();
}

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, \Tests\Models\User|\Tests\Models\Post, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, $this>
*/
public function countryWithCustomForeignKeys(): BelongsToThrough
{
Expand All @@ -37,7 +37,7 @@ public function countryWithCustomForeignKeys(): BelongsToThrough
}

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, \Tests\Models\User|\Tests\Models\Post, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, $this>
*/
public function countryWithTrashedUser(): BelongsToThrough
{
Expand All @@ -46,24 +46,24 @@ public function countryWithTrashedUser(): BelongsToThrough
}

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, \Tests\Models\User|\Tests\Models\Post, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\Country, $this>
*/
public function countryWithPrefix(): BelongsToThrough
{
return $this->belongsToThrough(Country::class, [User::class, Post::class], null, 'custom_');
}

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<self, self, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<self, $this>
*/
public function grandparent(): BelongsToThrough
{
/* @phpstan-ignore argument.templateType, argument.type, return.type */
/* @phpstan-ignore argument.type */
return $this->belongsToThrough(self::class, self::class.' as alias', null, '', [self::class => 'parent_id']);
}

/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\User, \Tests\Models\Post, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\User, $this>
*/
public function user(): BelongsToThrough
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Models/CustomerAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class CustomerAddress extends Model
{
/**
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\VendorCustomer, \Tests\Models\VendorCustomerAddress, $this>
* @return \Znck\Eloquent\Relations\BelongsToThrough<\Tests\Models\VendorCustomer, $this>
*/
public function vendorCustomer(): BelongsToThrough
{
Expand Down

0 comments on commit 28cbfca

Please sign in to comment.