Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relation generics and PHPStan lvl 9 #101

Merged
merged 4 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
*/
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>[]
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
*/
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
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
*
* @extends \Illuminate\Database\Eloquent\Relations\Relation<TRelatedModel>
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
*/
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[]
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
*/
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
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
* @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
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
* @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
*/
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
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
staudenmeir marked this conversation as resolved.
Show resolved Hide resolved
* @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