Skip to content

Commit

Permalink
Relation generics and PHPStan lvl 9
Browse files Browse the repository at this point in the history
  • Loading branch information
SanderMuller committed Aug 31, 2024
1 parent 33f1bd0 commit 18c5813
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 98 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 Relation<\Illuminate\Database\Eloquent\Model> $relationship
*/
protected function addRelationship(ModelsCommand $command, ReflectionMethod $method, Relation $relationship): void
{
$type = '\\' . $relationship->getRelated()::class;
Expand Down
6 changes: 5 additions & 1 deletion src/IdeHelperServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Staudenmeir\BelongsToThrough;

use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
use Illuminate\Console\Command;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
use Staudenmeir\BelongsToThrough\IdeHelper\BelongsToThroughRelationsHook;
Expand All @@ -18,11 +19,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<Command>[]
*/
public function provides(): array
{
return [
Expand Down
78 changes: 50 additions & 28 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,16 +299,22 @@ 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> $parent
* @param string[]|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>
*/
public function getRelationExistenceQuery(Builder $query, Builder $parent, $columns = ['*'])
{
$this->performJoins($query);

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

if ($from instanceof Expression) {
$from = $from->getValue($query->getGrammar());
}

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

return $query->select($columns)->whereColumn(
$this->getQualifiedFirstLocalKeyName(),
Expand All @@ -311,12 +326,13 @@ public function getRelationExistenceQuery(Builder $query, Builder $parent, $colu
/**
* 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 +342,7 @@ public function withTrashed(...$columns)
$columns = $columns[0];
}

/** @var string[] $columns */
foreach ($columns as $column) {
$this->query->withoutGlobalScope(__CLASS__ . ":$column");
}
Expand All @@ -336,7 +353,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 +367,11 @@ public function getThroughParents()
*/
public function getFirstForeignKeyName()
{
return $this->prefix . $this->getForeignKeyName(end($this->throughParents));
// TODO: the method docblock doesn't match with the usage of `end($this->throughParents)`
/** @var TIntermediateModel $lastThroughParent */
$lastThroughParent = end($this->throughParents);

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

/**
Expand All @@ -360,6 +381,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 18c5813

Please sign in to comment.