Skip to content

Commit 40fdd96

Browse files
committed
feat: get array types working
1 parent 13bd6fa commit 40fdd96

14 files changed

+675
-25
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
/composer.lock
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"autoload": {
3+
"psr-4": {
4+
"App\\": "src/"
5+
}
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Parameter \#1 \$relations of method App\\Builder\<App\\User\>\:\:with\(\) expects array\<string, Closure\(App\\Relation\<\*, \*, \*\>\)\: mixed\>, array\{car\: Closure\(App\\HasOne\)\: App\\HasOne, monitorable\: Closure\(App\\MorphTo\)\: App\\MorphTo\} given\.$#'
5+
identifier: argument.type
6+
count: 1
7+
path: src/test.php
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
includes:
2+
- phpstan-baseline.neon
3+
parameters:
4+
level: 9
5+
paths:
6+
- src
7+
services:
8+
-
9+
class: App\ParameterTypeExtension
10+
tags:
11+
- phpstan.dynamicMethodParameterTypeExtension
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\Native\NativeParameterReflection;
10+
use PHPStan\Reflection\ParameterReflection;
11+
use PHPStan\Reflection\PassedByReference;
12+
use PHPStan\Type\ClosureType;
13+
use PHPStan\Type\Constant\ConstantArrayType;
14+
use PHPStan\Type\Constant\ConstantStringType;
15+
use PHPStan\Type\DynamicMethodParameterTypeExtension;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\NeverType;
18+
use PHPStan\Type\ObjectType;
19+
use PHPStan\Type\StringType;
20+
use PHPStan\Type\Type;
21+
use PHPStan\Type\TypeCombinator;
22+
use PhpParser\Node\Expr\MethodCall;
23+
use PhpParser\Node\Expr\StaticCall;
24+
use PhpParser\Node\Name;
25+
use PhpParser\Node\VariadicPlaceholder;
26+
27+
final class ParameterTypeExtension implements DynamicMethodParameterTypeExtension
28+
{
29+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
30+
{
31+
if (! $methodReflection->getDeclaringClass()->is(Builder::class)) {
32+
return false;
33+
}
34+
35+
return $methodReflection->getName() === 'with';
36+
}
37+
38+
public function getTypeFromMethodCall(
39+
MethodReflection $methodReflection,
40+
MethodCall $methodCall,
41+
ParameterReflection $parameter,
42+
Scope $scope,
43+
): Type|null {
44+
$arg = $methodCall->getArgs()[0] ?? null;
45+
if (!$arg) {
46+
return null;
47+
}
48+
49+
$type = $scope->getType($arg->value)->getConstantArrays()[0] ?? null;
50+
if (!$type) {
51+
return null;
52+
}
53+
54+
$model = $scope->getType($methodCall->var)
55+
->getTemplateType(Builder::class, 'TModel')
56+
->getObjectClassNames()[0] ?? null;
57+
if (!$model) {
58+
return null;
59+
}
60+
61+
foreach ($type->getKeyTypes() as $keyType) {
62+
$relationType = $this->getRelationTypeFromModel($model, (string) $keyType->getValue(), $scope);
63+
if (!$relationType) {
64+
continue;
65+
}
66+
67+
$newType = new ClosureType([
68+
/** @phpstan-ignore phpstanApi.constructor */
69+
new NativeParameterReflection('test', false, $relationType, PassedByReference::createNo(), false, null),
70+
], new MixedType(), false);
71+
72+
$type = $type->setOffsetValueType($keyType, $newType, false);
73+
}
74+
75+
return $type;
76+
}
77+
78+
public function getRelationTypeFromModel(string $model, string $relation, Scope $scope): ?Type
79+
{
80+
$modelType = new ObjectType($model);
81+
82+
if (! $modelType->hasMethod($relation)->yes()) {
83+
return null;
84+
}
85+
86+
$relationType = $modelType->getMethod($relation, $scope)->getVariants()[0]->getReturnType();
87+
88+
if (! (new ObjectType(Relation::class))->isSuperTypeOf($relationType)->yes()) {
89+
return null;
90+
}
91+
92+
return $relationType;
93+
}
94+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace App;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
abstract class Model {}
8+
9+
class Monitor extends Model {}
10+
11+
class Car extends Model {}
12+
13+
class User extends Model
14+
{
15+
/** @return HasOne<Car, $this> */
16+
public function car(): HasOne
17+
{
18+
return new HasOne(); // @phpstan-ignore return.type
19+
}
20+
21+
/** @return MorphTo<Monitor, $this> */
22+
public function monitorable(): MorphTo
23+
{
24+
return new MorphTo(); // @phpstan-ignore return.type
25+
}
26+
}
27+
28+
/**
29+
* @template TRelatedModel of Model
30+
* @template TDeclaringModel of Model
31+
* @template TResult
32+
*/
33+
class Relation {
34+
/**
35+
* @param list<string> $columns
36+
* @return $this
37+
*/
38+
public function select(array $columns): static
39+
{
40+
return $this;
41+
}
42+
}
43+
44+
/**
45+
* @template TRelatedModel of Model
46+
* @template TDeclaringModel of Model
47+
* @extends Relation<TRelatedModel, TDeclaringModel, ?TRelatedModel>
48+
*/
49+
class HasOne extends Relation {}
50+
51+
/**
52+
* @template TRelatedModel of Model
53+
* @template TDeclaringModel of Model
54+
* @extends Relation<TRelatedModel, TDeclaringModel, ?TRelatedModel>
55+
*/
56+
class MorphTo extends Relation {
57+
/** @return $this */
58+
public function morphWith(): static
59+
{
60+
return $this;
61+
}
62+
}
63+
64+
/** @template TModel of Model */
65+
class Builder
66+
{
67+
/**
68+
* @param array<string, \Closure(Relation<*, *, *>): mixed> $relations
69+
* @return $this
70+
*/
71+
public function with(array $relations): static
72+
{
73+
return $this;
74+
}
75+
}
76+
77+
/** @param Builder<User> $query */
78+
function test(Builder $query): void
79+
{
80+
$query->with([
81+
'car' => function ($r) { assertType('App\HasOne<App\Car, App\User>', $r); },
82+
'monitorable' => function ($r) { assertType('App\MorphTo<App\Monitor, App\User>', $r); },
83+
]);
84+
$query->with([
85+
'car' => fn (HasOne $q) => $q->select(['id']),
86+
'monitorable' => fn (MorphTo $q) => $q->morphWith(),
87+
]);
88+
}

src/Analyser/ExpressionContext.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ private function __construct(
1212
private ?string $inAssignRightSideVariableName,
1313
private ?Type $inAssignRightSideType,
1414
private ?Type $inAssignRightSideNativeType,
15+
private ?Type $overriddenType = null,
1516
)
1617
{
1718
}
1819

1920
public static function createTopLevel(): self
2021
{
21-
return new self(false, null, null, null);
22+
return new self(false, null, null, null, null);
2223
}
2324

2425
public static function createDeep(): self
2526
{
26-
return new self(true, null, null, null);
27+
return new self(true, null, null, null, null);
2728
}
2829

2930
public function enterDeep(): self
@@ -32,7 +33,7 @@ public function enterDeep(): self
3233
return $this;
3334
}
3435

35-
return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType);
36+
return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType, $this->overriddenType);
3637
}
3738

3839
public function isDeep(): bool
@@ -42,7 +43,7 @@ public function isDeep(): bool
4243

4344
public function enterRightSideAssign(string $variableName, Type $type, Type $nativeType): self
4445
{
45-
return new self($this->isDeep, $variableName, $type, $nativeType);
46+
return new self($this->isDeep, $variableName, $type, $nativeType, $this->overriddenType);
4647
}
4748

4849
public function getInAssignRightSideVariableName(): ?string
@@ -60,4 +61,14 @@ public function getInAssignRightSideNativeType(): ?Type
6061
return $this->inAssignRightSideNativeType;
6162
}
6263

64+
public function withOverriddenType(?Type $type): self
65+
{
66+
return new self($this->isDeep, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType, $type);
67+
}
68+
69+
public function getOverriddenType(): ?Type
70+
{
71+
return $this->overriddenType;
72+
}
73+
6374
}

0 commit comments

Comments
 (0)