Skip to content

Commit

Permalink
Relation generics and PHPStan lvl 9 (#101)
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

---------

Co-authored-by: Jonas Staudenmeir <[email protected]>
  • Loading branch information
SanderMuller and staudenmeir authored Sep 4, 2024
1 parent 33f1bd0 commit b71fc5b
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 103 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"larastan/larastan": "^2.9",
"orchestra/testbench": "^9.0",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-phpunit": "^1.4",
"phpunit/phpunit": "^11.0"
},
"autoload": {
Expand Down
5 changes: 4 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
includes:
- ./vendor/larastan/larastan/extension.neon
- ./vendor/phpstan/phpstan-mockery/extension.neon
- ./vendor/phpstan/phpstan-phpunit/extension.neon
- ./vendor/phpstan/phpstan-phpunit/rules.neon
parameters:
level: 1
level: 9
paths:
- src
- tests
3 changes: 3 additions & 0 deletions src/IdeHelper/BelongsToThroughRelationsHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public function run(ModelsCommand $command, Model $model): void
}
}

/**
* @param \Illuminate\Database\Eloquent\Relations\Relation<\Illuminate\Database\Eloquent\Model> $relationship
*/
protected function addRelationship(ModelsCommand $command, ReflectionMethod $method, Relation $relationship): void
{
$type = '\\' . $relationship->getRelated()::class;
Expand Down
5 changes: 4 additions & 1 deletion src/IdeHelperServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ public function register(): void
'ide-helper.model_hooks',
array_merge(
[BelongsToThroughRelationsHook::class],
$config->get('ide-helper.model_hooks', [])
$config->array('ide-helper.model_hooks', [])
)
);
}

/**
* @return class-string<\Illuminate\Console\Command>[]
*/
public function provides(): array
{
return [
Expand Down
86 changes: 56 additions & 30 deletions src/Relations/BelongsToThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Str;

/**
* @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>
*/
class BelongsToThrough extends Relation
{
use SupportsDefaultModels;
Expand All @@ -24,7 +32,7 @@ class BelongsToThrough extends Relation
/**
* The "through" parent model instances.
*
* @var \Illuminate\Database\Eloquent\Model[]
* @var TIntermediateModel[]
*/
protected $throughParents;

Expand All @@ -38,27 +46,27 @@ class BelongsToThrough extends Relation
/**
* The custom foreign keys on the relationship.
*
* @var array
* @var array<string, string>
*/
protected $foreignKeyLookup;

/**
* The custom local keys on the relationship.
*
* @var array
* @var array<string, string>
*/
protected $localKeyLookup;

/**
* 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[] $throughParents
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TDeclaringModel $parent
* @param TIntermediateModel[] $throughParents
* @param string|null $localKey
* @param string $prefix
* @param array $foreignKeyLookup
* @param array $localKeyLookup
* @param array<string, string> $foreignKeyLookup
* @param array<string, string> $localKeyLookup
* @return void
*
* @phpstan-ignore constructor.unusedParameter($localKey)
Expand Down Expand Up @@ -99,7 +107,7 @@ public function addConstraints()
/**
* Set the join clauses on the query.
*
* @param \Illuminate\Database\Eloquent\Builder|null $query
* @param \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>|null $query
* @return void
*/
protected function performJoins(?Builder $query = null)
Expand All @@ -116,6 +124,7 @@ protected function performJoins(?Builder $query = null)
$query->join($model->getTable(), $first, '=', $second);

if ($this->hasSoftDeletes($model)) {
/** @phpstan-ignore method.notFound */
$column = $model->getQualifiedDeletedAtColumn();

$query->withGlobalScope(__CLASS__ . ":{$column}", function (Builder $query) use ($column) {
Expand Down Expand Up @@ -173,7 +182,7 @@ public function hasSoftDeletes(Model $model)
/**
* Set the constraints for an eager load of the relation.
*
* @param array $models
* @param \Illuminate\Database\Eloquent\Model[] $models
* @return void
*/
public function addEagerConstraints(array $models)
Expand All @@ -188,7 +197,7 @@ public function addEagerConstraints(array $models)
*
* @param \Illuminate\Database\Eloquent\Model[] $models
* @param string $relation
* @return array
* @return \Illuminate\Database\Eloquent\Model[]
*/
public function initRelation(array $models, $relation)
{
Expand All @@ -203,9 +212,9 @@ public function initRelation(array $models, $relation)
* Match the eagerly loaded results to their parents.
*
* @param \Illuminate\Database\Eloquent\Model[] $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $results
* @param string $relation
* @return array
* @return \Illuminate\Database\Eloquent\Model[]
*/
public function match(array $models, Collection $results, $relation)
{
Expand All @@ -225,8 +234,8 @@ public function match(array $models, Collection $results, $relation)
/**
* Build model dictionary keyed by the relation's foreign key.
*
* @param \Illuminate\Database\Eloquent\Collection $results
* @return array
* @param \Illuminate\Database\Eloquent\Collection<array-key, \Illuminate\Database\Eloquent\Model> $results
* @return \Illuminate\Database\Eloquent\Model[]
*/
protected function buildDictionary(Collection $results)
{
Expand All @@ -244,7 +253,7 @@ protected function buildDictionary(Collection $results)
/**
* Get the results of the relationship.
*
* @return \Illuminate\Database\Eloquent\Model
* @return TRelatedModel|object|static|null
*/
public function getResults()
{
Expand All @@ -254,8 +263,8 @@ public function getResults()
/**
* Execute the query and get the first result.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Model|object|static|null
* @param string[] $columns
* @return TRelatedModel|object|static|null
*/
public function first($columns = ['*'])
{
Expand All @@ -269,8 +278,8 @@ public function first($columns = ['*'])
/**
* Execute the query as a "select" statement.
*
* @param array $columns
* @return \Illuminate\Database\Eloquent\Collection
* @param string[] $columns
* @return \Illuminate\Database\Eloquent\Collection<array-key, TRelatedModel>
*/
public function get($columns = ['*'])
{
Expand All @@ -290,33 +299,45 @@ public function get($columns = ['*'])
/**
* Add the constraints for a relationship query.
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $query
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder $parent
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
* @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>
*/
public function getRelationExistenceQuery(Builder $query, Builder $parent, $columns = ['*'])
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
$this->performJoins($query);

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

return $query->select($columns)->whereColumn(
if ($from instanceof Expression) {
$from = $from->getValue(
$parentQuery->getGrammar()
);
}

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

/** @var \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model> $query */
$query = $query->select($columns)->whereColumn(
$this->getQualifiedFirstLocalKeyName(),
'=',
$foreignKey
);

return $query;
}

/**
* Restore soft-deleted models.
*
* @param array|string ...$columns
* @param string[]|string ...$columns
* @return $this
*/
public function withTrashed(...$columns)
{
if (empty($columns)) {
/** @phpstan-ignore method.notFound */
$this->query->withTrashed();

return $this;
Expand All @@ -326,6 +347,7 @@ public function withTrashed(...$columns)
$columns = $columns[0];
}

/** @var string[] $columns */
foreach ($columns as $column) {
$this->query->withoutGlobalScope(__CLASS__ . ":$column");
}
Expand All @@ -336,7 +358,7 @@ public function withTrashed(...$columns)
/**
* Get the "through" parent model instances.
*
* @return \Illuminate\Database\Eloquent\Model[]
* @return TIntermediateModel[]
*/
public function getThroughParents()
{
Expand All @@ -350,7 +372,10 @@ public function getThroughParents()
*/
public function getFirstForeignKeyName()
{
return $this->prefix . $this->getForeignKeyName(end($this->throughParents));
/** @var TIntermediateModel $firstThroughParent */
$firstThroughParent = end($this->throughParents);

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

/**
Expand All @@ -360,6 +385,7 @@ public function getFirstForeignKeyName()
*/
public function getQualifiedFirstLocalKeyName()
{
/** @var TIntermediateModel $lastThroughParent */
$lastThroughParent = end($this->throughParents);

return $lastThroughParent->qualifyColumn($this->getLocalKeyName($lastThroughParent));
Expand Down
50 changes: 33 additions & 17 deletions src/Traits/BelongsToThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ trait BelongsToThrough
/**
* Define a belongs-to-through relationship.
*
* @param string $related
* @param array|string $through
* @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 string|null $localKey
* @param string $prefix
* @param array $foreignKeyLookup
* @param array $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough
* @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>
*/
public function belongsToThrough(
$related,
Expand All @@ -27,19 +30,25 @@ public function belongsToThrough(
$foreignKeyLookup = [],
array $localKeyLookup = []
) {
/** @var TRelatedModel $relatedInstance */
$relatedInstance = $this->newRelatedInstance($related);

/** @var TIntermediateModel[] $throughParents */
$throughParents = [];
$foreignKeys = [];

foreach ((array) $through as $model) {
$foreignKey = null;

if (is_array($model)) {
/** @var string $foreignKey */
$foreignKey = $model[1];

/** @var class-string<TIntermediateModel> $model */
$model = $model[0];
}

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

if ($foreignKey) {
Expand Down Expand Up @@ -67,8 +76,8 @@ public function belongsToThrough(
/**
* Map keys to an associative array where the key is the table name and the value is the key from the lookup.
*
* @param array $keyLookup
* @return array
* @param array<class-string<\Illuminate\Database\Eloquent\Model>, string> $keyLookup
* @return array<string, string>
*/
protected function mapKeys(array $keyLookup): array
{
Expand All @@ -89,14 +98,17 @@ protected function mapKeys(array $keyLookup): array
/**
* Create a through parent instance for a belongs-to-through relationship.
*
* @param string $model
* @return \Illuminate\Database\Eloquent\Model
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @param class-string<TModel> $model
* @return TModel
*/
protected function belongsToThroughParentInstance($model)
{
/** @var array{0: class-string<TModel>, 1?: string} $segments */
$segments = preg_split('/\s+as\s+/i', $model);

/** @var \Illuminate\Database\Eloquent\Model $instance */
/** @var TModel $instance */
$instance = new $segments[0]();

if (isset($segments[1])) {
Expand All @@ -109,14 +121,18 @@ protected function belongsToThroughParentInstance($model)
/**
* Instantiate a new BelongsToThrough relationship.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param \Illuminate\Database\Eloquent\Model[] $throughParents
* @param string $localKey
* @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 string|null $localKey
* @param string $prefix
* @param array $foreignKeyLookup
* @param array $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough
* @param array<string, string> $foreignKeyLookup
* @param array<string, string> $localKeyLookup
* @return \Znck\Eloquent\Relations\BelongsToThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
*/
protected function newBelongsToThrough(
Builder $query,
Expand Down
Loading

0 comments on commit b71fc5b

Please sign in to comment.