-
-
Notifications
You must be signed in to change notification settings - Fork 394
/
RelationDynamicMethodReturnTypeExtension.php
99 lines (80 loc) · 3.2 KB
/
RelationDynamicMethodReturnTypeExtension.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?php
declare(strict_types=1);
namespace NunoMaduro\Larastan\Types;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
class RelationDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
private ReflectionProvider $provider;
public function __construct(ReflectionProvider $provider)
{
$this->provider = $provider;
}
public function getClass(): string
{
return Model::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), [
'hasOne', 'hasOneThrough', 'morphOne',
'belongsTo', 'morphTo',
'hasMany', 'hasManyThrough', 'morphMany',
'belongsToMany', 'morphToMany', 'morphedByMany',
], true);
}
/**
* @throws ShouldNotHappenException
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
/** @var FunctionVariant $functionVariant */
$functionVariant = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
$returnType = $functionVariant->getReturnType();
if (! $returnType instanceof TypeWithClassName) {
return $returnType;
}
$calledOnType = $scope->getType($methodCall->var);
if ($calledOnType instanceof StaticType) {
$calledOnType = new ObjectType($calledOnType->getClassName());
}
if (count($methodCall->getArgs()) === 0) {
// Special case for MorphTo. `morphTo` can be called without arguments.
if ($methodReflection->getName() === 'morphTo') {
return new GenericObjectType($returnType->getClassName(), [new ObjectType(Model::class), $calledOnType]);
}
return $returnType;
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
$argStrings = TypeUtils::getConstantStrings($argType);
if (count($argStrings) !== 1) {
return $returnType;
}
$argClassName = $argStrings[0]->getValue();
if (! $this->provider->hasClass($argClassName)) {
$argClassName = Model::class;
}
// Special case for BelongsTo. We need to add the child model as a generic type also.
if ((new ObjectType(BelongsTo::class))->isSuperTypeOf($returnType)->yes()) {
return new GenericObjectType($returnType->getClassName(), [new ObjectType($argClassName), $calledOnType]);
}
return new GenericObjectType($returnType->getClassName(), [new ObjectType($argClassName)]);
}
}